From 41322f237a4c1acf3c76280261587507b8ff3864 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Mon, 13 Apr 2020 15:20:37 +0200 Subject: [PATCH] query - busy indicator, canceling --- packages/api/src/controllers/sessions.js | 10 ++++ packages/api/src/proc/sessionProcess.js | 15 +++++- packages/engines/mssql/index.js | 3 ++ packages/web/src/TabsPanel.js | 2 +- packages/web/src/appobj/AppObjects.js | 13 +++-- packages/web/src/appobj/openedTabAppObject.js | 4 +- packages/web/src/query/QueryToolbar.js | 9 +++- packages/web/src/sqleditor/ResultTabs.js | 4 +- packages/web/src/tabs/QueryTab.js | 49 ++++++++++++++----- packages/web/src/utility/common.js | 10 ++-- 10 files changed, 88 insertions(+), 31 deletions(-) diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index d30dfd5b..2c9fac8c 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -88,6 +88,16 @@ module.exports = { return { state: 'ok' }; }, + cancel_meta: 'post', + async cancel({ sesid }) { + const session = this.opened.find((x) => x.sesid == sesid); + if (!session) { + throw new Error('Invalid session'); + } + session.subprocess.send({ msgtype: 'cancel' }); + return { state: 'ok' }; + }, + // runCommand_meta: 'post', // async runCommand({ conid, database, sql }) { // console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 19cbc237..a6566ac5 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -9,6 +9,7 @@ const { jsldir } = require('../utility/directories'); let systemConnection; let storedConnection; let afterConnectCallbacks = []; +let currentHandlers = []; class StreamHandler { constructor() { @@ -17,6 +18,9 @@ class StreamHandler { // this.error = this.error.bind(this); this.done = this.done.bind(this); this.info = this.info.bind(this); + // use this for cancelling + this.stream = null; + currentHandlers = [...currentHandlers, this]; } closeCurrentStream() { @@ -44,6 +48,7 @@ class StreamHandler { done(result) { this.closeCurrentStream(); process.send({ msgtype: 'done', result }); + currentHandlers = currentHandlers.filter((x) => x != this); } info(info) { process.send({ msgtype: 'info', info }); @@ -61,6 +66,12 @@ async function handleConnect(connection) { afterConnectCallbacks = []; } +function handleCancel() { + for (const handler of currentHandlers) { + if (handler.stream) handler.stream.cancel(); + } +} + function waitConnected() { if (systemConnection) return Promise.resolve(); return new Promise((resolve, reject) => { @@ -73,12 +84,14 @@ async function handleExecuteQuery({ sql }) { const driver = engines(storedConnection); const handler = new StreamHandler(); - await driver.stream(systemConnection, sql, handler); + const stream = await driver.stream(systemConnection, sql, handler); + handler.stream = stream; } const messageHandlers = { connect: handleConnect, executeQuery: handleExecuteQuery, + cancel: handleCancel, }; async function handleMessage({ msgtype, ...other }) { diff --git a/packages/engines/mssql/index.js b/packages/engines/mssql/index.js index f672cc5f..43ef95ba 100644 --- a/packages/engines/mssql/index.js +++ b/packages/engines/mssql/index.js @@ -58,6 +58,7 @@ const driver = { user, password, database, + requestTimeout: 1000 * 3600, options: { enableArithAbort: true, }, @@ -150,6 +151,8 @@ const driver = { request.on('done', handleDone); request.on('info', handleInfo); request.query(sql); + + return request; }, async getVersion(pool) { const { version } = (await this.query(pool, 'SELECT @@VERSION AS version')).rows[0]; diff --git a/packages/web/src/TabsPanel.js b/packages/web/src/TabsPanel.js index 25970e9f..e1a934ec 100644 --- a/packages/web/src/TabsPanel.js +++ b/packages/web/src/TabsPanel.js @@ -91,7 +91,7 @@ export default function TabsPanel() { onClick={(e) => handleTabClick(e, tab.tabid)} onMouseUp={(e) => handleMouseUp(e, tab.tabid)} > - {getIconImage(tab.icon)} + {tab.busy ? : getIconImage(tab.icon)} {tab.title} (props.isBold ? 'bold' : 'normal')}; + font-weight: ${(props) => (props.isBold ? 'bold' : 'normal')}; `; const AppObjectSpan = styled.span` white-space: nowrap; - font-weight: ${props => (props.isBold ? 'bold' : 'normal')}; + font-weight: ${(props) => (props.isBold ? 'bold' : 'normal')}; `; const IconWrap = styled.span` @@ -33,13 +33,14 @@ export function AppObjectCore({ makeAppObj, onClick, isBold, + isBusy, component = 'div', prefix = null, ...other }) { const appObjectParams = useAppObjectParams(); - const handleContextMenu = event => { + const handleContextMenu = (event) => { if (!Menu) return; event.preventDefault(); @@ -60,9 +61,7 @@ export function AppObjectCore({ {...other} > {prefix} - - - + {isBusy ? : } {title} ); diff --git a/packages/web/src/appobj/openedTabAppObject.js b/packages/web/src/appobj/openedTabAppObject.js index f6db5e36..3fef7335 100644 --- a/packages/web/src/appobj/openedTabAppObject.js +++ b/packages/web/src/appobj/openedTabAppObject.js @@ -2,7 +2,7 @@ import React from 'react'; import _ from 'lodash'; import { getIconImage } from '../icons'; -const openedTabAppObject = () => ({ tabid, props, selected, icon, title }, { setOpenedTabs }) => { +const openedTabAppObject = () => ({ tabid, props, selected, icon, title, busy }, { setOpenedTabs }) => { const key = tabid; const Icon = (props) => getIconImage(icon, props); const isBold = !!selected; @@ -16,7 +16,7 @@ const openedTabAppObject = () => ({ tabid, props, selected, icon, title }, { set ); }; - return { title, key, Icon, isBold, onClick }; + return { title, key, Icon, isBold, onClick, isBusy: busy }; }; export default openedTabAppObject; diff --git a/packages/web/src/query/QueryToolbar.js b/packages/web/src/query/QueryToolbar.js index 8e5a7a00..02f19ec9 100644 --- a/packages/web/src/query/QueryToolbar.js +++ b/packages/web/src/query/QueryToolbar.js @@ -1,10 +1,15 @@ import React from 'react'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function QueryToolbar({ execute,isDatabaseDefined }) { +export default function QueryToolbar({ execute, cancel, isDatabaseDefined, busy }) { return ( <> - Execute + + Execute + + + Cancel + ); } diff --git a/packages/web/src/sqleditor/ResultTabs.js b/packages/web/src/sqleditor/ResultTabs.js index 86b19b3b..eea4e903 100644 --- a/packages/web/src/sqleditor/ResultTabs.js +++ b/packages/web/src/sqleditor/ResultTabs.js @@ -7,10 +7,10 @@ export default function ResultTabs({ children, sessionId, executeNumber }) { const socket = useSocket(); const [resultIds, setResultIds] = React.useState([]); - const handleResultSet = (props) => { + const handleResultSet = React.useCallback((props) => { const { jslid } = props; setResultIds((ids) => [...ids, jslid]); - }; + }, []); React.useEffect(() => { setResultIds([]); diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index 73f12b50..2d5661cf 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -4,24 +4,15 @@ import _ from 'lodash'; import axios from '../utility/axios'; import { useConnectionInfo } from '../utility/metadataLoaders'; import SqlEditor from '../sqleditor/SqlEditor'; -import { useUpdateDatabaseForTab } from '../utility/globalState'; +import { useUpdateDatabaseForTab, useSetOpenedTabs } from '../utility/globalState'; import QueryToolbar from '../query/QueryToolbar'; import SessionMessagesView from '../query/SessionMessagesView'; import { TabPage } from '../widgets/TabControl'; import ResultTabs from '../sqleditor/ResultTabs'; import { VerticalSplitter } from '../widgets/Splitter'; import keycodes from '../utility/keycodes'; - -// const MainContainer = styled.div``; - -// const EditorContainer = styled.div` -// height: 600px; -// position: relative; -// `; - -// const MessagesContainer = styled.div` -// height: 200px; -// `; +import { changeTab } from '../utility/common'; +import useSocket from '../utility/SocketProvider'; export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPortalRef, initialScript }) { const localStorageKey = `sql_${tabid}`; @@ -29,6 +20,9 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo const queryTextRef = React.useRef(queryText); const [sessionId, setSessionId] = React.useState(null); const [executeNumber, setExecuteNumber] = React.useState(0); + const setOpenedTabs = useSetOpenedTabs(); + const socket = useSocket(); + const [busy, setBusy] = React.useState(false); const saveToStorage = React.useCallback(() => localStorage.setItem(localStorageKey, queryTextRef.current), [ localStorageKey, @@ -44,6 +38,23 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo }; }, []); + const handleSessionDone = React.useCallback(() => { + setBusy(false); + }, []); + + React.useEffect(() => { + if (sessionId && socket) { + socket.on(`session-done-${sessionId}`, handleSessionDone); + return () => { + socket.off(`session-done-${sessionId}`, handleSessionDone); + }; + } + }, [sessionId, socket]); + + React.useEffect(() => { + changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, busy })); + }, [busy]); + const editorRef = React.useRef(null); useUpdateDatabaseForTab(tabVisible, conid, database); @@ -68,12 +79,19 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo sesid = resp.data.sesid; setSessionId(sesid); } + setBusy(true); await axios.post('sessions/execute-query', { sesid, sql: selectedText || queryText, }); }; + const handleCancel = () => { + axios.post('sessions/cancel', { + sesid: sessionId, + }); + }; + const handleKeyDown = (data, hash, keyString, keyCode, event) => { if (keyCode == keycodes.f5) { event.preventDefault(); @@ -113,7 +131,12 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo toolbarPortalRef.current && tabVisible && ReactDOM.createPortal( - , + , toolbarPortalRef.current )} diff --git a/packages/web/src/utility/common.js b/packages/web/src/utility/common.js index 2cc8af04..afd6699c 100644 --- a/packages/web/src/utility/common.js +++ b/packages/web/src/utility/common.js @@ -11,13 +11,13 @@ export class LoadingToken { } export function sleep(milliseconds) { - return new Promise(resolve => window.setTimeout(() => resolve(null), milliseconds)); + return new Promise((resolve) => window.setTimeout(() => resolve(null), milliseconds)); } export function openNewTab(setOpenedTabs, newTab) { const tabid = uuidv1(); - setOpenedTabs(files => [ - ...(files || []).map(x => ({ ...x, selected: false })), + setOpenedTabs((files) => [ + ...(files || []).map((x) => ({ ...x, selected: false })), { tabid, selected: true, @@ -25,3 +25,7 @@ export function openNewTab(setOpenedTabs, newTab) { }, ]); } + +export function changeTab(tabid, setOpenedTabs, changeFunc) { + setOpenedTabs((files) => files.map((tab) => (tab.tabid == tabid ? changeFunc(tab) : tab))); +}