diff --git a/app/README.md b/app/README.md new file mode 100644 index 000000000..fa3ff6e7d --- /dev/null +++ b/app/README.md @@ -0,0 +1,16 @@ +[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) +[![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/JanProchazkaCz/30eur) +[![NPM version](https://img.shields.io/npm/v/dbgate.svg)](https://www.npmjs.com/package/dbgate) + +# DbGate - database administration tool +DbGate is fast and easy to use database administration tool for MySQL, PostgreSQL, SQL Server. + +## Install using npm +Please download binary packages from https://dbgate.org . Or run from source code, as described on [github](https://github.com/dbgate/dbgate) + +## Other dbgate packages +You can use some functionality of dbgate from your JavaScript code. See [dbgate-api](https://npmjs.com/dbgate-api) package. + +## Screenshot + +![Screenshot](https://raw.githubusercontent.com/dbshell/dbgate/master/screenshot.png) diff --git a/app/package.json b/app/package.json index 83ebc9ba1..d170d04cc 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,6 @@ { "name": "dbgate", - "version": "3.9.3", - "private": true, + "version": "3.9.4-beta.5", "author": "Jan Prochazka ", "description": "Opensource database administration tool", "dependencies": { @@ -11,7 +10,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/dbshell/dbgate.git" + "url": "https://github.com/dbgate/dbgate.git" }, "build": { "appId": "org.dbgate", @@ -68,7 +67,7 @@ "devDependencies": { "copyfiles": "^2.2.0", "cross-env": "^6.0.3", - "electron": "11.1.1", + "electron": "11.2.1", "electron-builder": "22.9.1" }, "optionalDependencies": { diff --git a/app/src/electron.js b/app/src/electron.js index 9b95b162d..d6df7c9b1 100644 --- a/app/src/electron.js +++ b/app/src/electron.js @@ -1,6 +1,6 @@ const electron = require('electron'); const os = require('os'); -const { Menu } = require('electron'); +const { Menu, ipcMain } = require('electron'); const { fork } = require('child_process'); const { autoUpdater } = require('electron-updater'); const Store = require('electron-store'); @@ -20,6 +20,7 @@ const store = new Store(); // be closed automatically when the JavaScript object is garbage collected. let mainWindow; let splashWindow; +let mainMenu; log.transports.file.level = 'debug'; autoUpdater.logger = log; @@ -45,18 +46,63 @@ function buildMenu() { mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`); }, }, + { + label: 'Open file', + click() { + mainWindow.webContents.executeJavaScript(`dbgate_openFile()`); + }, + }, + { + label: 'Save', + click() { + mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`); + }, + accelerator: 'Ctrl+S', + id: 'save', + }, + { + label: 'Save As', + click() { + mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`); + }, + accelerator: 'Ctrl+Shift+S', + id: 'saveAs', + }, + { type: 'separator' }, + { role: 'close' }, + ], + }, + { + label: 'Window', + submenu: [ { label: 'New query', click() { mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`); }, }, + { type: 'separator' }, + { + label: 'Close all tabs', + click() { + mainWindow.webContents.executeJavaScript('dbgate_closeAll()'); + }, + }, + { role: 'minimize' }, ], }, - { - label: 'Edit', - submenu: [{ role: 'copy' }, { role: 'paste' }], - }, + + // { + // label: 'Edit', + // submenu: [ + // { role: 'undo' }, + // { role: 'redo' }, + // { type: 'separator' }, + // { role: 'cut' }, + // { role: 'copy' }, + // { role: 'paste' }, + // ], + // }, { label: 'View', submenu: [ @@ -71,20 +117,6 @@ function buildMenu() { { role: 'togglefullscreen' }, ], }, - { - role: 'window', - submenu: [ - { - label: 'Close all tabs', - click() { - mainWindow.webContents.executeJavaScript('dbgate_closeAll()'); - }, - }, - { type: 'separator' }, - { role: 'minimize' }, - { role: 'close' }, - ], - }, { role: 'help', submenu: [ @@ -97,7 +129,7 @@ function buildMenu() { { label: 'DbGate on GitHub', click() { - require('electron').shell.openExternal('https://github.com/dbshell/dbgate'); + require('electron').shell.openExternal('https://github.com/dbgate/dbgate'); }, }, { @@ -106,6 +138,12 @@ function buildMenu() { require('electron').shell.openExternal('https://hub.docker.com/r/dbgate/dbgate'); }, }, + { + label: 'Report problem or feature request', + click() { + require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new'); + }, + }, { label: 'About', click() { @@ -119,6 +157,12 @@ function buildMenu() { return Menu.buildFromTemplate(template); } +ipcMain.on('update-menu', async (event, arg) => { + const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`); + mainMenu.getMenuItemById('save').enabled = !!commands.save; + mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs; +}); + function createWindow() { const bounds = store.get('winBounds'); @@ -135,7 +179,8 @@ function createWindow() { }, }); - mainWindow.setMenu(buildMenu()); + mainMenu = buildMenu(); + mainWindow.setMenu(mainMenu); function loadMainWindow() { const startUrl = diff --git a/app/yarn.lock b/app/yarn.lock index 623f4561b..e99033a0b 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -717,10 +717,10 @@ electron-updater@^4.3.5: lodash.isequal "^4.5.0" semver "^7.3.2" -electron@11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-11.1.1.tgz#188f036f8282798398dca9513e9bb3b10213e3aa" - integrity sha512-tlbex3xosJgfileN6BAQRotevPRXB/wQIq48QeQ08tUJJrXwE72c8smsM/hbHx5eDgnbfJ2G3a60PmRjHU2NhA== +electron@11.2.1: + version "11.2.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-11.2.1.tgz#8641dd1a62911a1144e0c73c34fd9f37ccc65c2b" + integrity sha512-Im1y29Bnil+Nzs+FCTq01J1OtLbs+2ZGLLllaqX/9n5GgpdtDmZhS/++JHBsYZ+4+0n7asO+JKQgJD+CqPClzg== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index efb75d1bc..ad3e91cb2 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -78,6 +78,11 @@ module.exports = { } }, + saveAs_meta: 'post', + async saveAs({ filePath, data, format }) { + await fs.writeFile(filePath, serialize(format, data)); + }, + favorites_meta: 'get', async favorites() { if (!hasPermission(`files/favorites/read`)) return []; diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index f3bfd1bb9..86bcdf040 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -29,8 +29,8 @@ const hasPermission = require('../utility/hasPermission'); const preinstallPluginMinimalVersions = { 'dbgate-plugin-mssql': '1.0.10', - 'dbgate-plugin-mysql': '1.0.3', - 'dbgate-plugin-postgres': '1.0.2', + 'dbgate-plugin-mysql': '1.0.4', + 'dbgate-plugin-postgres': '1.0.3', 'dbgate-plugin-csv': '1.0.8', 'dbgate-plugin-excel': '1.0.6', }; diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index bbb058c5f..17c7154ef 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -8,6 +8,7 @@ const lock = new AsyncLock(); module.exports = { opened: [], closed: {}, + lastPinged: {}, handle_databases(conid, { databases }) { const existing = this.opened.find(x => x.conid == conid); @@ -88,7 +89,12 @@ module.exports = { ping_meta: 'post', async ping({ connections }) { await Promise.all( - connections.map(async conid => { + _.uniq(connections).map(async conid => { + const last = this.lastPinged[conid]; + if (last && new Date().getTime() - last < 30 * 1000) { + return Promise.resolve(); + } + this.lastPinged[conid] = new Date().getTime(); const opened = await this.ensureOpened(conid); opened.subprocess.send({ msgtype: 'ping' }); }) diff --git a/packages/tools/package.json b/packages/tools/package.json index a4b8569f9..d2e5e3ded 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,5 +1,5 @@ { - "version": "1.0.7", + "version": "1.0.8", "name": "dbgate-tools", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/tools/src/nameTools.ts b/packages/tools/src/nameTools.ts index cabecb01b..cebe2796a 100644 --- a/packages/tools/src/nameTools.ts +++ b/packages/tools/src/nameTools.ts @@ -49,3 +49,15 @@ export function findObjectLike( export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) { return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName)); } + +export function makeUniqueColumnNames(res: ColumnInfo[]) { + const usedNames = new Set(); + for (let i = 0; i < res.length; i++) { + if (usedNames.has(res[i].columnName)) { + let suffix = 2; + while (usedNames.has(`${res[i].columnName}${suffix}`)) suffix++; + res[i].columnName = `${res[i].columnName}${suffix}`; + } + usedNames.add(res[i].columnName); + } +} diff --git a/packages/web/src/DragAndDropFileTarget.js b/packages/web/src/DragAndDropFileTarget.js index 334eedbae..5741ca470 100644 --- a/packages/web/src/DragAndDropFileTarget.js +++ b/packages/web/src/DragAndDropFileTarget.js @@ -2,6 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import { FontIcon } from './icons'; import useTheme from './theme/useTheme'; +import getElectron from './utility/getElectron'; import useExtensions from './utility/useExtensions'; const TargetStyled = styled.div` @@ -41,6 +42,9 @@ const TitleWrapper = styled.div` export default function DragAndDropFileTarget({ isDragActive, inputProps }) { const theme = useTheme(); const { fileFormats } = useExtensions(); + const electron = getElectron(); + const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name); + if (electron) fileTypeNames.push('SQL'); return ( !!isDragActive && ( @@ -49,13 +53,7 @@ export default function DragAndDropFileTarget({ isDragActive, inputProps }) { Drop the files to upload to DbGate - - Supported file types:{' '} - {fileFormats - .filter(x => x.readerFunc) - .map(x => x.name) - .join(', ')} - + Supported file types: {fileTypeNames.join(', ')} diff --git a/packages/web/src/Screen.js b/packages/web/src/Screen.js index db7ebae6e..8299dce9f 100644 --- a/packages/web/src/Screen.js +++ b/packages/web/src/Screen.js @@ -100,6 +100,7 @@ export default function Screen() { ? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness : dimensions.widgetMenu.iconSize; const toolbarPortalRef = React.useRef(); + const statusbarPortalRef = React.useRef(); const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff)); const { getRootProps, getInputProps, isDragActive } = useUploadsZone(); @@ -131,10 +132,10 @@ export default function Screen() { - + - + diff --git a/packages/web/src/TabContent.js b/packages/web/src/TabContent.js index f4733d669..f90da055f 100644 --- a/packages/web/src/TabContent.js +++ b/packages/web/src/TabContent.js @@ -18,12 +18,18 @@ const TabContainerStyled = styled.div` `; function TabContainer({ TabComponent, ...props }) { - const { tabVisible, tabid, toolbarPortalRef } = props; + const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props; return ( // @ts-ignore - + ); @@ -42,7 +48,7 @@ function createTabComponent(selectedTab) { return null; } -export default function TabContent({ toolbarPortalRef }) { +export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) { const files = useOpenedTabs(); const [mountedTabs, setMountedTabs] = React.useState({}); @@ -84,6 +90,7 @@ export default function TabContent({ toolbarPortalRef }) { tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} + statusbarPortalRef={statusbarPortalRef} TabComponent={TabComponent} /> ); diff --git a/packages/web/src/TabsPanel.js b/packages/web/src/TabsPanel.js index c976e50e2..37c3e121d 100644 --- a/packages/web/src/TabsPanel.js +++ b/packages/web/src/TabsPanel.js @@ -10,6 +10,7 @@ import useTheme from './theme/useTheme'; import usePropsCompare from './utility/usePropsCompare'; import { useShowMenu } from './modals/showMenu'; import { setSelectedTabFunc } from './utility/common'; +import getElectron from './utility/getElectron'; // const files = [ // { name: 'app.js' }, @@ -124,6 +125,15 @@ function getDbIcon(key) { return 'icon file'; } +function buildTooltip(tab) { + let res = tab.tooltip; + if (tab.props && tab.props.savedFilePath) { + if (res) res += '\n'; + res += tab.props.savedFilePath; + } + return res; +} + export default function TabsPanel() { // const formatDbKey = (conid, database) => `${database}-${conid}`; const theme = useTheme(); @@ -210,6 +220,16 @@ export default function TabsPanel() { ); }; + React.useEffect(() => { + const electron = getElectron(); + if (electron) { + const { ipcRenderer } = electron; + const activeTab = tabs.find(x => x.selected); + window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null; + ipcRenderer.send('update-menu'); + } + }, [tabs]); + // console.log( // 't', // tabs.map(x => x.tooltip) @@ -252,7 +272,7 @@ export default function TabsPanel() { {_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => ( handleTabClick(e, tab.tabid)} diff --git a/packages/web/src/appobj/AppObjectCore.js b/packages/web/src/appobj/AppObjectCore.js index 15978b0a8..58efa6b2d 100644 --- a/packages/web/src/appobj/AppObjectCore.js +++ b/packages/web/src/appobj/AppObjectCore.js @@ -49,6 +49,7 @@ export function AppObjectCore({ extInfo = undefined, statusTitle = undefined, disableHover = false, + children = null, Menu = undefined, ...other }) { @@ -63,31 +64,34 @@ export function AppObjectCore({ }; return ( - { - if (onClick) onClick(data); - if (onClick2) onClick2(data); - if (onClick3) onClick3(data); - }} - theme={theme} - isBold={isBold} - draggable - onDragStart={e => { - e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data)); - }} - disableHover={disableHover} - {...other} - > - {prefix} - {isBusy ? : } - {title} - {statusIcon && ( - - - - )} - {extInfo && {extInfo}} - + <> + { + if (onClick) onClick(data); + if (onClick2) onClick2(data); + if (onClick3) onClick3(data); + }} + theme={theme} + isBold={isBold} + draggable + onDragStart={e => { + e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data)); + }} + disableHover={disableHover} + {...other} + > + {prefix} + {isBusy ? : } + {title} + {statusIcon && ( + + + + )} + {extInfo && {extInfo}} + + {children} + ); } diff --git a/packages/web/src/appobj/ClosedTabAppObject.js b/packages/web/src/appobj/ClosedTabAppObject.js index 360d9a2de..0ef7e11b9 100644 --- a/packages/web/src/appobj/ClosedTabAppObject.js +++ b/packages/web/src/appobj/ClosedTabAppObject.js @@ -5,6 +5,14 @@ import { DropDownMenuItem } from '../modals/DropDownMenu'; import { useSetOpenedTabs } from '../utility/globalState'; import { AppObjectCore } from './AppObjectCore'; import { setSelectedTabFunc } from '../utility/common'; +import styled from 'styled-components'; +import { FontIcon } from '../icons'; +import useTheme from '../theme/useTheme'; + +const InfoDiv = styled.div` + margin-left: 30px; + color: ${props => props.theme.left_font3}; +`; function Menu({ data }) { const setOpenedTabs = useSetOpenedTabs(); @@ -25,17 +33,16 @@ function Menu({ data }) { function ClosedTabAppObject({ data, commonProps }) { const { tabid, props, selected, icon, title, closedTime, busy } = data; const setOpenedTabs = useSetOpenedTabs(); + const theme = useTheme(); const onClick = () => { setOpenedTabs(files => setSelectedTabFunc( - files.map( - x => ({ - ...x, - closedTime: x.tabid == tabid ? undefined : x.closedTime, - }), - tabid - ) + files.map(x => ({ + ...x, + closedTime: x.tabid == tabid ? undefined : x.closedTime, + })), + tabid ) ); }; @@ -50,7 +57,14 @@ function ClosedTabAppObject({ data, commonProps }) { onClick={onClick} isBusy={busy} Menu={Menu} - /> + > + {data.props && data.props.database && ( + + {data.props.database} + + )} + {data.contentPreview && {data.contentPreview}} + ); } diff --git a/packages/web/src/appobj/ConnectionAppObject.js b/packages/web/src/appobj/ConnectionAppObject.js index b13313b26..4e5c7c3b0 100644 --- a/packages/web/src/appobj/ConnectionAppObject.js +++ b/packages/web/src/appobj/ConnectionAppObject.js @@ -40,7 +40,7 @@ function Menu({ data }) { setOpenedConnections(list => list.filter(x => x != data._id)); }; const handleConnect = () => { - setOpenedConnections(list => [...list, data._id]); + setOpenedConnections(list => _.uniq([...list, data._id])); }; return ( <> @@ -72,7 +72,7 @@ function ConnectionAppObject({ data, commonProps }) { const extensions = useExtensions(); const isBold = _.get(currentDatabase, 'connection._id') == _id; - const onClick = () => setOpenedConnections(c => [...c, _id]); + const onClick = () => setOpenedConnections(c => _.uniq([...c, _id])); let statusIcon = null; let statusTitle = null; diff --git a/packages/web/src/appobj/DatabaseAppObject.js b/packages/web/src/appobj/DatabaseAppObject.js index c8a137b5a..04cef6bd9 100644 --- a/packages/web/src/appobj/DatabaseAppObject.js +++ b/packages/web/src/appobj/DatabaseAppObject.js @@ -20,7 +20,7 @@ function Menu({ data }) { const handleNewQuery = () => { openNewTab({ - title: 'Query', + title: 'Query #', icon: 'img sql-file', tooltip, tabComponent: 'QueryTab', diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.js b/packages/web/src/appobj/DatabaseObjectAppObject.js index 88fc4badc..dbf5bff84 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.js +++ b/packages/web/src/appobj/DatabaseObjectAppObject.js @@ -149,7 +149,7 @@ export async function openDatabaseObjectDetail( openNewTab( { - title: pureName, + title: sqlTemplate ? 'Query #' : pureName, tooltip, icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField], tabComponent: sqlTemplate ? 'QueryTab' : tabComponent, @@ -245,7 +245,7 @@ function Menu({ data }) { } else if (menu.isQueryDesigner) { openNewTab( { - title: data.pureName, + title: 'Query #', icon: 'img query-design', tabComponent: 'QueryDesignTab', props: { diff --git a/packages/web/src/appobj/SavedFileAppObject.js b/packages/web/src/appobj/SavedFileAppObject.js index 244b7cd1d..3feb1e9fb 100644 --- a/packages/web/src/appobj/SavedFileAppObject.js +++ b/packages/web/src/appobj/SavedFileAppObject.js @@ -104,7 +104,7 @@ export function SavedSqlFileAppObject({ data, commonProps }) { openNewTab( { - title: 'Shell', + title: 'Shell #', icon: 'img shell', tabComponent: 'ShellTab', }, diff --git a/packages/web/src/charts/ChartToolbar.js b/packages/web/src/charts/ChartToolbar.js index fc60b4832..62b3a1aaf 100644 --- a/packages/web/src/charts/ChartToolbar.js +++ b/packages/web/src/charts/ChartToolbar.js @@ -1,17 +1,9 @@ import React from 'react'; -import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function ChartToolbar({ save, modelState, dispatchModel }) { - const hasPermission = useHasPermission(); - +export default function ChartToolbar({ modelState, dispatchModel }) { return ( <> - {hasPermission('files/charts/write') && ( - - Save - - )} dispatchModel({ type: 'undo' })} icon="icon undo"> Undo diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js index 63f0cc9f8..933089ce7 100644 --- a/packages/web/src/datagrid/DataGridCore.js +++ b/packages/web/src/datagrid/DataGridCore.js @@ -312,7 +312,7 @@ export default function DataGridCore(props) { } } : null, - [formViewAvailable, display] + [formViewAvailable, display, openNewTab] ); if (!columns || columns.length == 0) return ; @@ -353,7 +353,7 @@ export default function DataGridCore(props) { const handleOpenFreeTable = () => { openNewTab( { - title: 'selection', + title: 'Data #', icon: 'img free-table', tabComponent: 'FreeTableTab', props: {}, @@ -365,7 +365,7 @@ export default function DataGridCore(props) { const handleOpenChart = () => { openNewTab( { - title: 'Chart', + title: 'Chart #', icon: 'img chart', tabComponent: 'ChartTab', props: {}, diff --git a/packages/web/src/datagrid/SqlDataGridCore.js b/packages/web/src/datagrid/SqlDataGridCore.js index 62c02ec85..fd3fc60af 100644 --- a/packages/web/src/datagrid/SqlDataGridCore.js +++ b/packages/web/src/datagrid/SqlDataGridCore.js @@ -83,7 +83,7 @@ export default function SqlDataGridCore(props) { function openActiveChart() { openNewTab( { - title: 'Chart', + title: 'Chart #', icon: 'img chart', tabComponent: 'ChartTab', props: { @@ -104,7 +104,7 @@ export default function SqlDataGridCore(props) { function openQuery() { openNewTab( { - title: 'Query', + title: 'Query #', icon: 'img sql-file', tabComponent: 'QueryTab', props: { diff --git a/packages/web/src/designer/QueryDesignToolbar.js b/packages/web/src/designer/QueryDesignToolbar.js index 22c2fa3c4..c53092e69 100644 --- a/packages/web/src/designer/QueryDesignToolbar.js +++ b/packages/web/src/designer/QueryDesignToolbar.js @@ -1,18 +1,15 @@ import React from 'react'; -import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; export default function QueryDesignToolbar({ execute, isDatabaseDefined, busy, - save, modelState, dispatchModel, isConnected, kill, }) { - const hasPermission = useHasPermission(); return ( <> @@ -21,11 +18,6 @@ export default function QueryDesignToolbar({ Kill - {hasPermission('files/query/write') && ( - - Save - - )} dispatchModel({ type: 'undo' })} icon="icon undo"> Undo diff --git a/packages/web/src/freetable/MacroDetail.js b/packages/web/src/freetable/MacroDetail.js index ac222d204..078415135 100644 --- a/packages/web/src/freetable/MacroDetail.js +++ b/packages/web/src/freetable/MacroDetail.js @@ -15,7 +15,7 @@ const Container = styled.div` display: flex; justify-content: space-between; align-items: center; - background: #ddeeee; + background: ${props => props.theme.gridheader_background_cyan[0]}; height: ${dimensions.toolBar.height}px; min-height: ${dimensions.toolBar.height}px; overflow: hidden; diff --git a/packages/web/src/freetable/useNewFreeTable.js b/packages/web/src/freetable/useNewFreeTable.js index 947e4cde5..97830a607 100644 --- a/packages/web/src/freetable/useNewFreeTable.js +++ b/packages/web/src/freetable/useNewFreeTable.js @@ -6,7 +6,7 @@ export default function useNewFreeTable() { return ({ title = undefined, ...props } = {}) => openNewTab({ - title: title || 'Table', + title: title || 'Data #', icon: 'img free-table', tabComponent: 'FreeTableTab', props, diff --git a/packages/web/src/impexp/ImportExportConfigurator.js b/packages/web/src/impexp/ImportExportConfigurator.js index b575c0deb..17c843aac 100644 --- a/packages/web/src/impexp/ImportExportConfigurator.js +++ b/packages/web/src/impexp/ImportExportConfigurator.js @@ -411,7 +411,11 @@ function SourceName({ name }) { ); } -export default function ImportExportConfigurator({ uploadedFile = undefined, onChangePreview = undefined }) { +export default function ImportExportConfigurator({ + uploadedFile = undefined, + openedFile = undefined, + onChangePreview = undefined, +}) { const { values, setFieldValue, setValues } = useForm(); const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName }); const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId }); @@ -453,6 +457,21 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC if (uploadedFile) { handleUpload(uploadedFile); } + if (openedFile) { + addFilesToSourceList( + extensions, + [ + { + fileName: openedFile.filePath, + shortName: openedFile.shortName, + }, + ], + values, + setValues, + !sourceList || sourceList.length == 0 ? openedFile.storageType : null, + setPreviewSource + ); + } }, []); const supportsPreview = diff --git a/packages/web/src/index.js b/packages/web/src/index.js index eea7e10fa..6cb89510d 100644 --- a/packages/web/src/index.js +++ b/packages/web/src/index.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import _ from 'lodash'; import './index.css'; import '@mdi/font/css/materialdesignicons.css'; import App from './App'; @@ -22,6 +23,17 @@ import localStorageGarbageCollector from './utility/localStorageGarbageCollector // import 'ace-builds/src-noconflict/snippets/mysql'; localStorageGarbageCollector(); +window['dbgate_tabExports'] = {}; +window['dbgate_getCurrentTabCommands'] = () => { + const tabid = window['dbgate_activeTabId']; + return _.mapValues(window['dbgate_tabExports'][tabid] || {}, v => !!v); +}; +window['dbgate_tabCommand'] = cmd => { + const tabid = window['dbgate_activeTabId']; + const commands = window['dbgate_tabExports'][tabid]; + const func = (commands || {})[cmd]; + if (func) func(); +}; ReactDOM.render(, document.getElementById('root')); diff --git a/packages/web/src/markdown/MarkdownToolbar.js b/packages/web/src/markdown/MarkdownToolbar.js index 108d325ce..7c87dab63 100644 --- a/packages/web/src/markdown/MarkdownToolbar.js +++ b/packages/web/src/markdown/MarkdownToolbar.js @@ -1,17 +1,9 @@ import React from 'react'; -import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function MarkdownToolbar({ save, showPreview }) { - const hasPermission = useHasPermission(); - +export default function MarkdownToolbar({ showPreview }) { return ( <> - {hasPermission('files/markdown/write') && ( - - Save - - )} Preview diff --git a/packages/web/src/modals/ImportExportModal.js b/packages/web/src/modals/ImportExportModal.js index 17b34827c..b7b2b319a 100644 --- a/packages/web/src/modals/ImportExportModal.js +++ b/packages/web/src/modals/ImportExportModal.js @@ -100,7 +100,7 @@ function GenerateSctriptButton({ modalState }) { const code = await createImpExpScript(extensions, values); openNewTab( { - title: 'Shell', + title: 'Shell #', icon: 'img shell', tabComponent: 'ShellTab', }, @@ -120,6 +120,7 @@ export default function ImportExportModal({ modalState, initialValues, uploadedFile = undefined, + openedFile = undefined, importToArchive = false, }) { const [executeNumber, setExecuteNumber] = React.useState(0); @@ -195,7 +196,11 @@ export default function ImportExportModal({ Import/Export {busy && } - + diff --git a/packages/web/src/modals/SaveFileModal.js b/packages/web/src/modals/SaveFileModal.js index 583fb8078..db587740d 100644 --- a/packages/web/src/modals/SaveFileModal.js +++ b/packages/web/src/modals/SaveFileModal.js @@ -6,14 +6,51 @@ import ModalHeader from './ModalHeader'; import ModalContent from './ModalContent'; import ModalFooter from './ModalFooter'; import { FormProvider } from '../utility/FormProvider'; +import FormStyledButton from '../widgets/FormStyledButton'; +import getElectron from '../utility/getElectron'; + +export default function SaveFileModal({ + data, + folder, + format, + modalState, + name, + fileExtension, + filePath, + onSave = undefined, +}) { + const electron = getElectron(); -export default function SaveFileModal({ data, folder, format, modalState, name, onSave = undefined }) { const handleSubmit = async values => { const { name } = values; await axios.post('files/save', { folder, file: name, data, format }); modalState.close(); - if (onSave) onSave(name); + if (onSave) { + onSave(name, { + savedFile: name, + savedFolder: folder, + savedFilePath: null, + }); + } }; + + const handleSaveToDisk = async filePath => { + const path = window.require('path'); + const parsed = path.parse(filePath); + // if (!parsed.ext) filePath += `.${fileExtension}`; + + await axios.post('files/save-as', { filePath, data, format }); + modalState.close(); + + if (onSave) { + onSave(parsed.name, { + savedFile: null, + savedFolder: null, + savedFilePath: filePath, + }); + } + }; + return ( Save file @@ -23,6 +60,25 @@ export default function SaveFileModal({ data, folder, format, modalState, name, + {electron && ( + { + const file = electron.remote.dialog.showSaveDialogSync(electron.remote.getCurrentWindow(), { + filters: [ + { name: `${fileExtension.toUpperCase()} files`, extensions: [fileExtension] }, + { name: `All files`, extensions: ['*'] }, + ], + defaultPath: filePath || `${name}.${fileExtension}`, + properties: ['showOverwriteConfirmation'], + }); + if (file) { + handleSaveToDisk(file); + } + }} + /> + )} diff --git a/packages/web/src/modals/SaveTabModal.js b/packages/web/src/modals/SaveTabModal.js index d581649b4..c60b4a955 100644 --- a/packages/web/src/modals/SaveTabModal.js +++ b/packages/web/src/modals/SaveTabModal.js @@ -1,53 +1,117 @@ import React from 'react'; +import axios from '../utility/axios'; import { changeTab } from '../utility/common'; +import getElectron from '../utility/getElectron'; import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState'; import keycodes from '../utility/keycodes'; +import SaveFileToolbarButton from '../utility/SaveFileToolbarButton'; +import ToolbarPortal from '../utility/ToolbarPortal'; +import useHasPermission from '../utility/useHasPermission'; import SaveFileModal from './SaveFileModal'; +import useModalState from './useModalState'; -export default function SaveTabModal({ data, folder, format, modalState, tabid, tabVisible }) { +export default function SaveTabModal({ + data, + folder, + format, + tabid, + tabVisible, + fileExtension, + toolbarPortalRef = undefined, +}) { const setOpenedTabs = useSetOpenedTabs(); const openedTabs = useOpenedTabs(); + const saveFileModalState = useModalState(); + const hasPermission = useHasPermission(); + const canSave = hasPermission(`files/${folder}/write`); - const { savedFile } = openedTabs.find(x => x.tabid == tabid).props || {}; - const onSave = name => + const { savedFile, savedFilePath } = openedTabs.find(x => x.tabid == tabid).props || {}; + const onSave = (title, newProps) => { changeTab(tabid, setOpenedTabs, tab => ({ ...tab, - title: name, + title, props: { ...tab.props, - savedFile: name, - savedFolder: folder, savedFormat: format, + ...newProps, }, })); + }; + + const handleSave = async () => { + if (savedFile) { + await axios.post('files/save', { folder, file: savedFile, data, format }); + } + if (savedFilePath) { + await axios.post('files/save-as', { filePath: savedFilePath, data, format }); + } + }; + const handleSaveRef = React.useRef(handleSave); + handleSaveRef.current = handleSave; const handleKeyboard = React.useCallback( e => { if (e.keyCode == keycodes.s && e.ctrlKey) { e.preventDefault(); - modalState.open(); + if (e.shiftKey) { + saveFileModalState.open(); + } else { + if (savedFile || savedFilePath) handleSaveRef.current(); + else saveFileModalState.open(); + } } }, - [modalState] + [saveFileModalState] ); React.useEffect(() => { - if (tabVisible) { + if (tabVisible && canSave) { document.addEventListener('keydown', handleKeyboard); return () => { document.removeEventListener('keydown', handleKeyboard); }; } - }, [tabVisible, handleKeyboard]); + }, [tabVisible, handleKeyboard, canSave]); + + React.useEffect(() => { + const electron = getElectron(); + if (electron) { + const { ipcRenderer } = electron; + window['dbgate_tabExports'][tabid] = { + save: handleSaveRef.current, + saveAs: saveFileModalState.open, + }; + ipcRenderer.send('update-menu'); + + return () => { + delete window['dbgate_tabExports'][tabid]; + ipcRenderer.send('update-menu'); + }; + } + }, []); return ( - + <> + + + {canSave && ( + + + + )} + ); } diff --git a/packages/web/src/query/QueryToolbar.js b/packages/web/src/query/QueryToolbar.js index 72567f0f3..7c51409be 100644 --- a/packages/web/src/query/QueryToolbar.js +++ b/packages/web/src/query/QueryToolbar.js @@ -2,7 +2,7 @@ import React from 'react'; import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, format, isConnected, kill }) { +export default function QueryToolbar({ execute, isDatabaseDefined, busy, format, isConnected, kill }) { const hasPermission = useHasPermission(); return ( <> @@ -15,11 +15,11 @@ export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, f Kill - {hasPermission('files/sql/write') && ( + {/* {hasPermission('files/sql/write') && ( Save - )} + )} */} Format diff --git a/packages/web/src/query/ShellToolbar.js b/packages/web/src/query/ShellToolbar.js index f141a8bd7..a9c747f40 100644 --- a/packages/web/src/query/ShellToolbar.js +++ b/packages/web/src/query/ShellToolbar.js @@ -1,9 +1,7 @@ import React from 'react'; -import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; -export default function ShellToolbar({ execute, cancel, busy, edit, save, editAvailable }) { - const hasPermission = useHasPermission(); +export default function ShellToolbar({ execute, cancel, busy, edit, editAvailable }) { return ( <> @@ -15,11 +13,6 @@ export default function ShellToolbar({ execute, cancel, busy, edit, save, editAv Show wizard - {hasPermission('files/shell/write') && ( - - Save - - )} ); } diff --git a/packages/web/src/query/useNewQuery.js b/packages/web/src/query/useNewQuery.js index 53cdd749d..ee796771e 100644 --- a/packages/web/src/query/useNewQuery.js +++ b/packages/web/src/query/useNewQuery.js @@ -14,7 +14,7 @@ export default function useNewQuery() { return ({ title = undefined, initialData = undefined, ...props } = {}) => openNewTab( { - title: title || 'Query', + title: title || 'Query #', icon: 'img sql-file', tooltip, tabComponent: 'QueryTab', @@ -40,7 +40,7 @@ export function useNewQueryDesign() { return ({ title = undefined, initialData = undefined, ...props } = {}) => openNewTab( { - title: title || 'Query', + title: title || 'Query #', icon: 'img query-design', tooltip, tabComponent: 'QueryDesignTab', diff --git a/packages/web/src/tabs/ChartTab.js b/packages/web/src/tabs/ChartTab.js index 3a3882dc4..934ce1279 100644 --- a/packages/web/src/tabs/ChartTab.js +++ b/packages/web/src/tabs/ChartTab.js @@ -4,17 +4,16 @@ import { createFreeTableModel } from 'dbgate-datalib'; import useUndoReducer from '../utility/useUndoReducer'; import ReactDOM from 'react-dom'; import { useUpdateDatabaseForTab } from '../utility/globalState'; -import useModalState from '../modals/useModalState'; import LoadingInfo from '../widgets/LoadingInfo'; import ErrorInfo from '../widgets/ErrorInfo'; import useEditorData from '../utility/useEditorData'; import SaveTabModal from '../modals/SaveTabModal'; import ChartEditor from '../charts/ChartEditor'; import ChartToolbar from '../charts/ChartToolbar'; +import ToolbarPortal from '../utility/ToolbarPortal'; export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database, tabid }) { const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel()); - const saveFileModalState = useModalState(); const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({ tabid, }); @@ -57,20 +56,17 @@ export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database database={database} /> - {toolbarPortalRef && - toolbarPortalRef.current && - tabVisible && - ReactDOM.createPortal( - , - toolbarPortalRef.current - )} + + + ); } diff --git a/packages/web/src/tabs/FreeTableTab.js b/packages/web/src/tabs/FreeTableTab.js index a366820b9..7495c885b 100644 --- a/packages/web/src/tabs/FreeTableTab.js +++ b/packages/web/src/tabs/FreeTableTab.js @@ -15,7 +15,7 @@ import useEditorData from '../utility/useEditorData'; export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, toolbarPortalRef, tabid, initialArgs }) { const [config, setConfig] = useGridConfig(tabid); const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel()); - const saveFileModalState = useModalState(); + const saveArchiveModalState = useModalState(); const setOpenedTabs = useSetOpenedTabs(); const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({ tabid, @@ -59,9 +59,9 @@ export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, to dispatchModel={dispatchModel} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} - onSave={() => saveFileModalState.open()} + onSave={() => saveArchiveModalState.open()} /> - + ); } diff --git a/packages/web/src/tabs/MarkdownEditorTab.js b/packages/web/src/tabs/MarkdownEditorTab.js index b45f1e936..63241a8e7 100644 --- a/packages/web/src/tabs/MarkdownEditorTab.js +++ b/packages/web/src/tabs/MarkdownEditorTab.js @@ -11,10 +11,10 @@ import LoadingInfo from '../widgets/LoadingInfo'; import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState'; import useOpenNewTab from '../utility/useOpenNewTab'; import { setSelectedTabFunc } from '../utility/common'; +import ToolbarPortal from '../utility/ToolbarPortal'; export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) { const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid }); - const saveFileModalState = useModalState(); const openedTabs = useOpenedTabs(); const setOpenedTabs = useSetOpenedTabs(); const openNewTab = useOpenNewTab(); @@ -61,20 +61,17 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, onKeyDown={handleKeyDown} mode="markdown" /> - {toolbarPortalRef && - toolbarPortalRef.current && - tabVisible && - ReactDOM.createPortal( - , - toolbarPortalRef.current - )} + + + ); diff --git a/packages/web/src/tabs/QueryDesignTab.js b/packages/web/src/tabs/QueryDesignTab.js index c50ff5bdc..c9b9648a8 100644 --- a/packages/web/src/tabs/QueryDesignTab.js +++ b/packages/web/src/tabs/QueryDesignTab.js @@ -15,7 +15,6 @@ import keycodes from '../utility/keycodes'; import { changeTab } from '../utility/common'; import useSocket from '../utility/SocketProvider'; import SaveTabModal from '../modals/SaveTabModal'; -import useModalState from '../modals/useModalState'; import sqlFormatter from 'sql-formatter'; import useEditorData from '../utility/useEditorData'; import LoadingInfo from '../widgets/LoadingInfo'; @@ -25,15 +24,25 @@ import QueryDesignColumns from '../designer/QueryDesignColumns'; import { findEngineDriver } from 'dbgate-tools'; import { generateDesignedQuery } from '../designer/designerTools'; import useUndoReducer from '../utility/useUndoReducer'; +import { StatusBarItem } from '../widgets/StatusBar'; +import useTimerLabel from '../utility/useTimerLabel'; +import ToolbarPortal from '../utility/ToolbarPortal'; -export default function QueryDesignTab({ tabid, conid, database, tabVisible, toolbarPortalRef, ...other }) { +export default function QueryDesignTab({ + tabid, + conid, + database, + tabVisible, + toolbarPortalRef, + statusbarPortalRef, + ...other +}) { const [sessionId, setSessionId] = React.useState(null); const [visibleResultTabs, setVisibleResultTabs] = React.useState(false); const [executeNumber, setExecuteNumber] = React.useState(0); const setOpenedTabs = useSetOpenedTabs(); const socket = useSocket(); const [busy, setBusy] = React.useState(false); - const saveFileModalState = useModalState(); const extensions = useExtensions(); const connection = useConnectionInfo({ conid }); const engine = findEngineDriver(connection, extensions); @@ -49,6 +58,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too }, { mergeNearActions: true } ); + const timerLabel = useTimerLabel(); React.useEffect(() => { // @ts-ignore @@ -61,6 +71,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too const handleSessionDone = React.useCallback(() => { setBusy(false); + timerLabel.stop(); }, []); const generatePreview = (value, engine) => { @@ -114,6 +125,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too setSessionId(sesid); } setBusy(true); + timerLabel.start(); await axios.post('sessions/execute-query', { sesid, sql: sqlPreview, @@ -126,6 +138,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too }); setSessionId(null); setBusy(false); + timerLabel.stop(); }; const handleKeyDown = React.useCallback( @@ -182,31 +195,32 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too )} - {toolbarPortalRef && - toolbarPortalRef.current && + + + + {statusbarPortalRef && + statusbarPortalRef.current && tabVisible && - ReactDOM.createPortal( - , - toolbarPortalRef.current - )} + ReactDOM.createPortal({timerLabel.text}, statusbarPortalRef.current)} ); diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index a945fc861..8f19529a1 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -21,16 +21,46 @@ import useEditorData from '../utility/useEditorData'; import applySqlTemplate from '../utility/applySqlTemplate'; import LoadingInfo from '../widgets/LoadingInfo'; import useExtensions from '../utility/useExtensions'; +import useTimerLabel from '../utility/useTimerLabel'; +import { StatusBarItem } from '../widgets/StatusBar'; +import ToolbarPortal from '../utility/ToolbarPortal'; -export default function QueryTab({ tabid, conid, database, initialArgs, tabVisible, toolbarPortalRef, ...other }) { +function createSqlPreview(sql) { + if (!sql) return undefined; + let data = sql.substring(0, 500); + data = data.replace(/\[[^\]]+\]\./g, ''); + data = data.replace(/\[a-zA-Z0-9_]+\./g, ''); + data = data.replace(/\/\*.*\*\//g, ''); + data = data.replace(/[\[\]]/g, ''); + data = data.replace(/--[^\n]*\n/g, ''); + + for (let step = 1; step <= 5; step++) { + data = data.replace(/\([^\(^\)]+\)/g, ''); + } + data = data.replace(/\s+/g, ' '); + data = data.trim(); + data = data.replace(/^(.{50}[^\s]*).*/, '$1'); + return data; +} + +export default function QueryTab({ + tabid, + conid, + database, + initialArgs, + tabVisible, + toolbarPortalRef, + statusbarPortalRef, + ...other +}) { const [sessionId, setSessionId] = React.useState(null); const [visibleResultTabs, setVisibleResultTabs] = React.useState(false); const [executeNumber, setExecuteNumber] = React.useState(0); const setOpenedTabs = useSetOpenedTabs(); const socket = useSocket(); const [busy, setBusy] = React.useState(false); - const saveFileModalState = useModalState(); const extensions = useExtensions(); + const timerLabel = useTimerLabel(); const { editorData, setEditorData, isLoading } = useEditorData({ tabid, loadFromArgs: @@ -43,6 +73,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib const handleSessionDone = React.useCallback(() => { setBusy(false); + timerLabel.stop(); }, []); React.useEffect(() => { @@ -61,6 +92,23 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib useUpdateDatabaseForTab(tabVisible, conid, database); const connection = useConnectionInfo({ conid }); + const updateContentPreviewDebounced = React.useRef( + _.debounce( + // @ts-ignore + sql => + changeTab(tabid, setOpenedTabs, tab => ({ + ...tab, + contentPreview: createSqlPreview(sql), + })), + 500 + ) + ); + + React.useEffect(() => { + // @ts-ignore + updateContentPreviewDebounced.current(editorData); + }, [editorData]); + const handleExecute = async () => { if (busy) return; setExecuteNumber(num => num + 1); @@ -77,6 +125,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib setSessionId(sesid); } setBusy(true); + timerLabel.start(); await axios.post('sessions/execute-query', { sesid, sql: selectedText || editorData, @@ -95,6 +144,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib }); setSessionId(null); setBusy(false); + timerLabel.stop(); }; const handleKeyDown = (data, hash, keyString, keyCode, event) => { @@ -151,7 +201,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib )} - {toolbarPortalRef && + {/* {toolbarPortalRef && toolbarPortalRef.current && tabVisible && ReactDOM.createPortal( @@ -166,14 +216,31 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib kill={handleKill} />, toolbarPortalRef.current - )} + )} */} + {statusbarPortalRef && + statusbarPortalRef.current && + tabVisible && + ReactDOM.createPortal({timerLabel.text}, statusbarPortalRef.current)} + + + ); diff --git a/packages/web/src/tabs/ShellTab.js b/packages/web/src/tabs/ShellTab.js index e54fa675d..3f9174b6d 100644 --- a/packages/web/src/tabs/ShellTab.js +++ b/packages/web/src/tabs/ShellTab.js @@ -14,18 +14,20 @@ import useShowModal from '../modals/showModal'; import ImportExportModal from '../modals/ImportExportModal'; import useEditorData from '../utility/useEditorData'; import SaveTabModal from '../modals/SaveTabModal'; -import useModalState from '../modals/useModalState'; import LoadingInfo from '../widgets/LoadingInfo'; +import useTimerLabel from '../utility/useTimerLabel'; +import { StatusBarItem } from '../widgets/StatusBar'; +import ToolbarPortal from '../utility/ToolbarPortal'; const configRegex = /\s*\/\/\s*@ImportExportConfigurator\s*\n\s*\/\/\s*(\{[^\n]+\})\n/; const requireRegex = /\s*(\/\/\s*@require\s+[^\n]+)\n/g; const initRegex = /([^\n]+\/\/\s*@init)/g; -export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other }) { +export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, statusbarPortalRef, ...other }) { const [busy, setBusy] = React.useState(false); const showModal = useShowModal(); const { editorData, setEditorData, isLoading } = useEditorData({ tabid }); - const saveFileModalState = useModalState(); + const timerLabel = useTimerLabel(); const setOpenedTabs = useSetOpenedTabs(); @@ -42,6 +44,7 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other const handleRunnerDone = React.useCallback(() => { setBusy(false); + timerLabel.stop(); }, []); React.useEffect(() => { @@ -69,12 +72,14 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other runid = resp.data.runid; setRunnerId(runid); setBusy(true); + timerLabel.start(); }; const handleCancel = () => { axios.post('runners/cancel', { runid: runnerId, }); + timerLabel.stop(); }; const handleKeyDown = (data, hash, keyString, keyCode, event) => { @@ -114,27 +119,27 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other /> - {toolbarPortalRef && - toolbarPortalRef.current && + + + + {statusbarPortalRef && + statusbarPortalRef.current && tabVisible && - ReactDOM.createPortal( - , - toolbarPortalRef.current - )} + ReactDOM.createPortal({timerLabel.text}, statusbarPortalRef.current)} ); diff --git a/packages/web/src/utility/ConnectionsPinger.js b/packages/web/src/utility/ConnectionsPinger.js index 0af32169a..67784ca45 100644 --- a/packages/web/src/utility/ConnectionsPinger.js +++ b/packages/web/src/utility/ConnectionsPinger.js @@ -7,9 +7,11 @@ export default function ConnectionsPinger({ children }) { const openedConnections = useOpenedConnections(); const currentDatabase = useCurrentDatabase(); - const doPing = () => { + const doServerPing = () => { axios.post('server-connections/ping', { connections: openedConnections }); + }; + const doDatabasePing = () => { const database = _.get(currentDatabase, 'name'); const conid = _.get(currentDatabase, 'connection._id'); if (conid && database) { @@ -18,9 +20,16 @@ export default function ConnectionsPinger({ children }) { }; React.useEffect(() => { - doPing(); - const handle = window.setInterval(doPing, 30 * 1000); + doServerPing(); + const handle = window.setInterval(doServerPing, 30 * 1000); return () => window.clearInterval(handle); - }, [openedConnections, currentDatabase]); + }, [openedConnections]); + + React.useEffect(() => { + doDatabasePing(); + const handle = window.setInterval(doDatabasePing, 30 * 1000); + return () => window.clearInterval(handle); + }, [currentDatabase]); + return children; } diff --git a/packages/web/src/utility/SaveFileToolbarButton.js b/packages/web/src/utility/SaveFileToolbarButton.js new file mode 100644 index 000000000..d4af2f855 --- /dev/null +++ b/packages/web/src/utility/SaveFileToolbarButton.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { DropDownMenuItem } from '../modals/DropDownMenu'; +import ToolbarButton, { ToolbarDropDownButton } from '../widgets/ToolbarButton'; + +export default function SaveFileToolbarButton({ tabid, save, saveAs }) { + if (!saveAs) return null; + + if (save) { + return ( + + + Save + + + Save As + + + ); + } + + return ( + + Save As + + ); +} diff --git a/packages/web/src/utility/ToolbarPortal.js b/packages/web/src/utility/ToolbarPortal.js new file mode 100644 index 000000000..ba184681f --- /dev/null +++ b/packages/web/src/utility/ToolbarPortal.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +export default function ToolbarPortal({ toolbarPortalRef, tabVisible, children }) { + return ( + (toolbarPortalRef && + toolbarPortalRef.current && + tabVisible && + children && + ReactDOM.createPortal(children, toolbarPortalRef.current)) || + null + ); +} diff --git a/packages/web/src/utility/UploadsProvider.js b/packages/web/src/utility/UploadsProvider.js index 3e4d31de3..726a54dbd 100644 --- a/packages/web/src/utility/UploadsProvider.js +++ b/packages/web/src/utility/UploadsProvider.js @@ -3,8 +3,10 @@ import { useDropzone } from 'react-dropzone'; import ImportExportModal from '../modals/ImportExportModal'; import useShowModal from '../modals/showModal'; import { findFileFormat } from './fileformats'; +import getElectron from './getElectron'; import resolveApi from './resolveApi'; import useExtensions from './useExtensions'; +import { useOpenElectronFileCore, canOpenByElectron } from './useOpenElectronFile'; const UploadsContext = React.createContext(null); @@ -21,6 +23,8 @@ export function useUploadFiles() { const { uploadListener } = useUploadsProvider(); const showModal = useShowModal(); const extensions = useExtensions(); + const electron = getElectron(); + const openElectronFileCore = useOpenElectronFileCore(); const handleUploadFiles = React.useCallback( files => { @@ -31,6 +35,12 @@ export function useUploadFiles() { } console.log('FILE', file); + + if (electron && canOpenByElectron(file.path, extensions)) { + openElectronFileCore(file.path); + return; + } + const formData = new FormData(); formData.append('data', file); diff --git a/packages/web/src/utility/useEditorData.js b/packages/web/src/utility/useEditorData.js index aa5d8df1c..c6b80b9ef 100644 --- a/packages/web/src/utility/useEditorData.js +++ b/packages/web/src/utility/useEditorData.js @@ -43,6 +43,8 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n setValue(init); valueRef.current = init; initialDataRef.current = init; + // mark as not saved + changeCounterRef.current += 1; } catch (err) { const message = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed'; setErrorMessage(message); diff --git a/packages/web/src/utility/useOpenElectronFile.js b/packages/web/src/utility/useOpenElectronFile.js new file mode 100644 index 000000000..5240cca00 --- /dev/null +++ b/packages/web/src/utility/useOpenElectronFile.js @@ -0,0 +1,88 @@ +import _ from 'lodash'; +import React from 'react'; +import ImportExportModal from '../modals/ImportExportModal'; +import useShowModal from '../modals/showModal'; +import useNewQuery from '../query/useNewQuery'; +import getElectron from './getElectron'; +import useExtensions from './useExtensions'; + +export function canOpenByElectron(file, extensions) { + if (!file) return false; + const nameLower = file.toLowerCase(); + if (nameLower.endsWith('.sql')) return true; + for (const format of extensions.fileFormats) { + if (nameLower.endsWith(`.${format.extension}`)) return true; + } + return false; +} + +export function useOpenElectronFileCore() { + const newQuery = useNewQuery(); + const extensions = useExtensions(); + const showModal = useShowModal(); + + return filePath => { + const nameLower = filePath.toLowerCase(); + const path = window.require('path'); + const fs = window.require('fs'); + const parsed = path.parse(filePath); + + if (nameLower.endsWith('.sql')) { + const data = fs.readFileSync(filePath, { encoding: 'utf-8' }); + + newQuery({ + title: parsed.name, + initialData: data, + // @ts-ignore + savedFilePath: filePath, + savedFormat: 'text', + }); + } + for (const format of extensions.fileFormats) { + if (nameLower.endsWith(`.${format.extension}`)) { + showModal(modalState => ( + + )); + } + } + }; +} + +function getFileFormatFilters(extensions) { + return extensions.fileFormats.filter(x => x.readerFunc).map(x => ({ name: x.name, extensions: [x.extension] })); +} + +function getFileFormatExtensions(extensions) { + return extensions.fileFormats.filter(x => x.readerFunc).map(x => x.extension); +} + +export default function useOpenElectronFile() { + const electron = getElectron(); + const openElectronFileCore = useOpenElectronFileCore(); + const extensions = useExtensions(); + + return () => { + const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), { + filters: [ + { name: `All supported files`, extensions: ['sql', ...getFileFormatExtensions(extensions)] }, + { name: `SQL files`, extensions: ['sql'] }, + ...getFileFormatFilters(extensions), + ], + }); + const filePath = filePaths && filePaths[0]; + if (canOpenByElectron(filePath, extensions)) { + openElectronFileCore(filePath); + } + }; +} diff --git a/packages/web/src/utility/useOpenNewTab.js b/packages/web/src/utility/useOpenNewTab.js index 611b8fd61..2c20dc0d7 100644 --- a/packages/web/src/utility/useOpenNewTab.js +++ b/packages/web/src/utility/useOpenNewTab.js @@ -7,6 +7,14 @@ import { useOpenedTabs, useSetOpenedTabs } from './globalState'; import tabs from '../tabs'; import { setSelectedTabFunc } from './common'; +function findFreeNumber(numbers) { + if (numbers.length == 0) return 1; + return _.max(numbers) + 1; + // let res = 1; + // while (numbers.includes(res)) res += 1; + // return res; +} + export default function useOpenNewTab() { const setOpenedTabs = useSetOpenedTabs(); const openedTabs = useOpenedTabs(); @@ -15,11 +23,16 @@ export default function useOpenNewTab() { async (newTab, initialData = undefined, options) => { let existing = null; - const { savedFile } = newTab.props || {}; - if (savedFile) { + const { savedFile, savedFolder, savedFilePath } = newTab.props || {}; + if (savedFile || savedFilePath) { existing = openedTabs.find( x => - x.props && x.tabComponent == newTab.tabComponent && x.closedTime == null && x.props.savedFile == savedFile + x.props && + x.tabComponent == newTab.tabComponent && + x.closedTime == null && + x.props.savedFile == savedFile && + x.props.savedFolder == savedFolder && + x.props.savedFilePath == savedFilePath ); } @@ -42,6 +55,15 @@ export default function useOpenNewTab() { return; } + // new tab will be created + if (newTab.title.endsWith('#')) { + const numbers = openedTabs + .filter(x => x.closedTime == null && x.title && x.title.startsWith(newTab.title)) + .map(x => parseInt(x.title.substring(newTab.title.length))); + + newTab.title = `${newTab.title}${findFreeNumber(numbers)}`; + } + const tabid = uuidv1(); if (initialData) { for (const key of _.keys(initialData)) { @@ -61,7 +83,7 @@ export default function useOpenNewTab() { }, ]); }, - [setOpenedTabs] + [setOpenedTabs, openedTabs] ); return openNewTab; diff --git a/packages/web/src/utility/useStorage.js b/packages/web/src/utility/useStorage.js index cd132c917..175820faf 100644 --- a/packages/web/src/utility/useStorage.js +++ b/packages/web/src/utility/useStorage.js @@ -16,12 +16,16 @@ export default function useStorage(key, storageObject, initialValue) { } }); + // use storedValue to ref, so that setValue with function argument works without changeing setValue itself + const storedValueRef = React.useRef(storedValue); + storedValueRef.current = storedValue; + // Return a wrapped version of useState's setter function that ... // ... persists the new value to localStorage. - const setValue = value => { + const setValue = React.useCallback(value => { try { // Allow value to be a function so we have same API as useState - const valueToStore = value instanceof Function ? value(storedValue) : value; + const valueToStore = value instanceof Function ? value(storedValueRef.current) : value; // Save state setStoredValue(valueToStore); // Save to local storage @@ -31,7 +35,7 @@ export default function useStorage(key, storageObject, initialValue) { console.error(error); console.log('Error saving storage value', key, value); } - }; + }, []); return [storedValue, setValue]; } diff --git a/packages/web/src/utility/useTimerLabel.js b/packages/web/src/utility/useTimerLabel.js new file mode 100644 index 000000000..f8c976e93 --- /dev/null +++ b/packages/web/src/utility/useTimerLabel.js @@ -0,0 +1,37 @@ +import React from 'react'; +import _ from 'lodash'; + +function formatSeconds(duration) { + if (duration == null) return ''; + const hours = _.padStart(Math.floor(duration / 3600).toString(), 2, '0'); + const minutes = _.padStart((Math.floor(duration / 60) % 60).toString(), 2, '0'); + const seconds = _.padStart((duration % 60).toString(), 2, '0'); + return `${hours}:${minutes}:${seconds}`; +} + +export default function useTimerLabel() { + const [duration, setDuration] = React.useState(null); + const [busy, setBusy] = React.useState(false); + React.useEffect(() => { + if (busy) { + setDuration(0); + const handle = setInterval(() => setDuration(x => x + 1), 1000); + return () => window.clearInterval(handle); + } + }, [busy]); + + const start = React.useCallback(() => { + setBusy(true); + }, []); + + const stop = React.useCallback(() => { + setBusy(false); + }, []); + + return { + start, + stop, + text: formatSeconds(duration), + duration, + }; +} diff --git a/packages/web/src/widgets/FavoritesWidget.js b/packages/web/src/widgets/FavoritesWidget.js index 27e67da31..ce0f3191f 100644 --- a/packages/web/src/widgets/FavoritesWidget.js +++ b/packages/web/src/widgets/FavoritesWidget.js @@ -44,14 +44,14 @@ export default function FavoritesWidget() { const hasPermission = useHasPermission(); return ( - - - {hasPermission('files/favorites/read') && ( - + )} + + + ); } diff --git a/packages/web/src/widgets/StatusBar.js b/packages/web/src/widgets/StatusBar.js index 097b4c135..621c320e9 100644 --- a/packages/web/src/widgets/StatusBar.js +++ b/packages/web/src/widgets/StatusBar.js @@ -10,12 +10,11 @@ const Container = styled.div` display: flex; color: ${props => props.theme.statusbar_font1}; align-items: stretch; + justify-content: space-between; `; -const Item = styled.div` +export const StatusBarItem = styled.div` padding: 2px 10px; - // margin: auto; - // flex-grow: 0; `; const ErrorWrapper = styled.span` @@ -30,62 +29,69 @@ const InfoWrapper = styled.span` props.theme.statusbar_font_green[5]}; `; -export default function StatusBar() { +const StatusbarContainer = styled.div` + display: flex; +`; + +export default function StatusBar({ statusbarPortalRef }) { const { name, connection } = useCurrentDatabase() || {}; const status = useDatabaseStatus(connection ? { conid: connection._id, database: name } : {}); const { displayName, server, user, engine } = connection || {}; const theme = useTheme(); return ( - {name && ( - - {name} - - )} - {(displayName || server) && ( - - {displayName || server} - - )} + + {name && ( + + {name} + + )} + {(displayName || server) && ( + + {displayName || server} + + )} - {user && ( - - {user} - - )} + {user && ( + + {user} + + )} - {connection && status && ( - - {status.name == 'pending' && ( + {connection && status && ( + + {status.name == 'pending' && ( + <> + Loading + + )} + {status.name == 'ok' && ( + <> + + + {' '} + Connected + + )} + {status.name == 'error' && ( + <> + + + {' '} + Error + + )} + + )} + {!connection && ( + <> - Loading + Not connected - )} - {status.name == 'ok' && ( - <> - - - {' '} - Connected - - )} - {status.name == 'error' && ( - <> - - - {' '} - Error - - )} - - )} - {!connection && ( - - <> - Not connected - - - )} + + )} + + ); } diff --git a/packages/web/src/widgets/Toolbar.js b/packages/web/src/widgets/Toolbar.js index fd01432ce..3873c1edc 100644 --- a/packages/web/src/widgets/Toolbar.js +++ b/packages/web/src/widgets/Toolbar.js @@ -25,6 +25,7 @@ import tabs from '../tabs'; import FavoriteModal from '../modals/FavoriteModal'; import { useOpenFavorite } from '../appobj/FavoriteFileAppObject'; import ErrorMessageModal from '../modals/ErrorMessageModal'; +import useOpenElectronFile from '../utility/useOpenElectronFile'; const ToolbarContainer = styled.div` display: flex; @@ -48,6 +49,7 @@ export default function ToolBar({ toolbarPortalRef }) { const electron = getElectron(); const favorites = useFavorites(); const openFavorite = useOpenFavorite(); + const openElectronFile = useOpenElectronFile(); const currentTab = openedTabs.find(x => x.selected); @@ -58,6 +60,7 @@ export default function ToolBar({ toolbarPortalRef }) { window['dbgate_newQuery'] = newQuery; window['dbgate_closeAll'] = () => setOpenedTabs([]); window['dbgate_showAbout'] = showAbout; + window['dbgate_openFile'] = openElectronFile; }); const showAbout = () => { @@ -91,7 +94,7 @@ export default function ToolBar({ toolbarPortalRef }) { const newMarkdown = () => { openNewTab({ - title: 'Page', + title: 'Page #', tabComponent: 'MarkdownEditorTab', icon: 'img markdown', }); @@ -103,7 +106,7 @@ export default function ToolBar({ toolbarPortalRef }) { const newShell = () => { openNewTab({ - title: 'Shell', + title: 'Shell #', icon: 'img shell', tabComponent: 'ShellTab', });