diff --git a/packages/api/src/controllers/runners.js b/packages/api/src/controllers/runners.js index 1c3e618f1..977fd85fb 100644 --- a/packages/api/src/controllers/runners.js +++ b/packages/api/src/controllers/runners.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const path = require('path'); -const fs = require('fs'); +const fs = require('fs-extra'); const uuidv1 = require('uuid/v1'); const socket = require('../utility/socket'); const { fork } = require('child_process'); @@ -83,11 +83,26 @@ module.exports = { cancel_meta: 'post', async cancel({ runid }) { - const session = this.opened.find((x) => x.runid == runid); - if (!session) { + const runner = this.opened.find((x) => x.runid == runid); + if (!runner) { throw new Error('Invalid runner'); } - session.subprocess.kill(); + runner.subprocess.kill(); return { state: 'ok' }; }, + + files_meta: 'get', + async files({ runid }) { + const directory = path.join(rundir(), runid); + const files = await fs.readdir(directory); + const res = []; + for (const file of files) { + const stat = await fs.stat(path.join(directory, file)); + res.push({ + name: file, + size: stat.size, + }); + } + return res; + }, }; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 9c91c33e8..838d0883e 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -19,6 +19,8 @@ const runners = require('./controllers/runners'); const jsldata = require('./controllers/jsldata'); const config = require('./controllers/config'); +const { rundir } = require('./utility/directories'); + function start(argument = null) { // console.log('process.argv', process.argv); @@ -43,6 +45,8 @@ function start(argument = null) { app.use('/pages', express.static(process.env.PAGES_DIRECTORY)); } + app.use('/runners/data', express.static(rundir())); + if (fs.existsSync('/home/dbgate-docker/build')) { // server static files inside docker container app.use(express.static('/home/dbgate-docker/build')); @@ -54,7 +58,7 @@ function start(argument = null) { if (argument == '--dynport') { childProcessChecker(); - + findFreePort(53911, function (err, port) { server.listen(port, () => { console.log(`DbGate API listening on port ${port}`); diff --git a/packages/web/src/modals/ImportExportModal.js b/packages/web/src/modals/ImportExportModal.js index 042e1dc94..ba7fe4506 100644 --- a/packages/web/src/modals/ImportExportModal.js +++ b/packages/web/src/modals/ImportExportModal.js @@ -42,7 +42,7 @@ export default function ImportExportModal({ modalState, initialValues }) { - + diff --git a/packages/web/src/query/MessagesView.js b/packages/web/src/query/MessagesView.js index fb5358e4a..d7e02511f 100644 --- a/packages/web/src/query/MessagesView.js +++ b/packages/web/src/query/MessagesView.js @@ -51,7 +51,7 @@ function formatDuration(duration) { return `${Math.round(duration / 1000)} s`; } -function MessagesView({ items, onMessageClick }) { +function MessagesView({ items, onMessageClick, showProcedure = false, showLine = false }) { const handleClick = (row) => { if (onMessageClick) onMessageClick(row); }; @@ -76,8 +76,8 @@ function MessagesView({ items, onMessageClick }) { Time Delta Duration - Procedure - Line + {showProcedure && Procedure} + {showLine && Line} {items.map((row, index) => ( // @ts-ignore @@ -91,8 +91,8 @@ function MessagesView({ items, onMessageClick }) { ? formatDuration(new Date(row.time).getTime() - new Date(items[index - 1].time).getTime()) : 'n/a'} - {row.procedure} - {row.line} + {showProcedure && {row.procedure}} + {showLine && {row.line}} ))} @@ -100,4 +100,4 @@ function MessagesView({ items, onMessageClick }) { ); } -export default React.memo(MessagesView); \ No newline at end of file +export default React.memo(MessagesView); diff --git a/packages/web/src/query/RunnerOuputFiles.js b/packages/web/src/query/RunnerOuputFiles.js new file mode 100644 index 000000000..ed1cd9233 --- /dev/null +++ b/packages/web/src/query/RunnerOuputFiles.js @@ -0,0 +1,46 @@ +import React from 'react'; +import useSocket from '../utility/SocketProvider'; +import axios from '../utility/axios'; +import styled from 'styled-components'; +import TableControl, { TableColumn } from '../utility/TableControl'; +import formatFileSize from '../utility/formatFileSize'; +import resolveApi from '../utility/resolveApi'; + +export default function RunnerOutputFiles({ runnerId, executeNumber }) { + const socket = useSocket(); + const [files, setFiles] = React.useState([]); + + const handleRunnerDone = React.useCallback(async () => { + const resp = await axios.get(`runners/files?runid=${runnerId}`); + setFiles(resp.data); + }, [runnerId]); + + React.useEffect(() => { + if (runnerId && socket) { + socket.on(`runner-done-${runnerId}`, handleRunnerDone); + return () => { + socket.off(`runner-done-${runnerId}`, handleRunnerDone); + }; + } + }, [runnerId, socket]); + + React.useEffect(() => { + setFiles([]); + }, [executeNumber]); + + return ( + + + formatFileSize(row.size)} /> + ( + + download + + )} + /> + + ); +} diff --git a/packages/web/src/query/RunnerOutputPane.js b/packages/web/src/query/RunnerOutputPane.js new file mode 100644 index 000000000..01e49bf9f --- /dev/null +++ b/packages/web/src/query/RunnerOutputPane.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { HorizontalSplitter } from '../widgets/Splitter'; +import SocketMessagesView from './SocketMessagesView'; +import { WidgetTitle } from '../widgets/WidgetStyles'; +import RunnerOutputFiles from './RunnerOuputFiles'; +import styled from 'styled-components'; + +const Container = styled.div` + flex: 1; + display: flex; + flex-direction: column; +`; + +export default function RunnerOutputPane({ runnerId, executeNumber }) { + return ( + + + Messages + + + + Output files + + + + ); +} diff --git a/packages/web/src/query/SocketMessagesView.js b/packages/web/src/query/SocketMessagesView.js index 616368b0f..ed0d55171 100644 --- a/packages/web/src/query/SocketMessagesView.js +++ b/packages/web/src/query/SocketMessagesView.js @@ -3,7 +3,13 @@ import React from 'react'; import MessagesView from './MessagesView'; import useSocket from '../utility/SocketProvider'; -export default function SocketMessagesView({ eventName, onMessageClick = undefined, executeNumber }) { +export default function SocketMessagesView({ + eventName, + onMessageClick = undefined, + executeNumber, + showProcedure = false, + showLine = false, +}) { const [displayedMessages, setDisplayedMessages] = React.useState([]); const cachedMessagesRef = React.useRef([]); const socket = useSocket(); @@ -35,5 +41,12 @@ export default function SocketMessagesView({ eventName, onMessageClick = undefin } }, [eventName, socket]); - return ; + return ( + + ); } diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index 7cede062d..c2a72d085 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -209,6 +209,8 @@ export default function QueryTab({ eventName={sessionId ? `session-info-${sessionId}` : null} onMessageClick={handleMesageClick} executeNumber={executeNumber} + showProcedure + showLine /> diff --git a/packages/web/src/tabs/ShellTab.js b/packages/web/src/tabs/ShellTab.js index fd729462c..72e6de3a9 100644 --- a/packages/web/src/tabs/ShellTab.js +++ b/packages/web/src/tabs/ShellTab.js @@ -11,7 +11,7 @@ import QueryToolbar from '../query/QueryToolbar'; import SocketMessagesView from '../query/SocketMessagesView'; import { TabPage } from '../widgets/TabControl'; import ResultTabs from '../sqleditor/ResultTabs'; -import { VerticalSplitter } from '../widgets/Splitter'; +import { VerticalSplitter, HorizontalSplitter } from '../widgets/Splitter'; import keycodes from '../utility/keycodes'; import { changeTab } from '../utility/common'; import useSocket from '../utility/SocketProvider'; @@ -20,6 +20,7 @@ import useModalState from '../modals/useModalState'; import sqlFormatter from 'sql-formatter'; import JavaScriptEditor from '../sqleditor/JavaScriptEditor'; import ShellToolbar from '../query/ShellToolbar'; +import RunnerOutputPane from '../query/RunnerOutputPane'; export default function ShellTab({ tabid, @@ -128,17 +129,13 @@ export default function ShellTab({ onKeyDown={handleKeyDown} editorRef={editorRef} /> - + {toolbarPortalRef && toolbarPortalRef.current && tabVisible && ReactDOM.createPortal( - , + , toolbarPortalRef.current )} diff --git a/packages/web/src/utility/formatFileSize.js b/packages/web/src/utility/formatFileSize.js new file mode 100644 index 000000000..059569559 --- /dev/null +++ b/packages/web/src/utility/formatFileSize.js @@ -0,0 +1,6 @@ +export default function formatFileSize(size) { + if (size > 1000000000) return `${Math.round(size / 10000000000) * 10} GB`; + if (size > 1000000) return `${Math.round(size / 10000000) * 10} MB`; + if (size > 1000) return `${Math.round(size / 10000) * 10} KB`; + return `${size} bytes`; +}