Merge branch 'develop'

This commit is contained in:
Jan Prochazka
2021-02-01 18:40:20 +01:00
52 changed files with 837 additions and 286 deletions

16
app/README.md Normal file
View File

@@ -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)

View File

@@ -1,7 +1,6 @@
{ {
"name": "dbgate", "name": "dbgate",
"version": "3.9.3", "version": "3.9.4-beta.5",
"private": true,
"author": "Jan Prochazka <jenasoft.database@gmail.com>", "author": "Jan Prochazka <jenasoft.database@gmail.com>",
"description": "Opensource database administration tool", "description": "Opensource database administration tool",
"dependencies": { "dependencies": {
@@ -11,7 +10,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/dbshell/dbgate.git" "url": "https://github.com/dbgate/dbgate.git"
}, },
"build": { "build": {
"appId": "org.dbgate", "appId": "org.dbgate",
@@ -68,7 +67,7 @@
"devDependencies": { "devDependencies": {
"copyfiles": "^2.2.0", "copyfiles": "^2.2.0",
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"electron": "11.1.1", "electron": "11.2.1",
"electron-builder": "22.9.1" "electron-builder": "22.9.1"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@@ -1,6 +1,6 @@
const electron = require('electron'); const electron = require('electron');
const os = require('os'); const os = require('os');
const { Menu } = require('electron'); const { Menu, ipcMain } = require('electron');
const { fork } = require('child_process'); const { fork } = require('child_process');
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
const Store = require('electron-store'); const Store = require('electron-store');
@@ -20,6 +20,7 @@ const store = new Store();
// be closed automatically when the JavaScript object is garbage collected. // be closed automatically when the JavaScript object is garbage collected.
let mainWindow; let mainWindow;
let splashWindow; let splashWindow;
let mainMenu;
log.transports.file.level = 'debug'; log.transports.file.level = 'debug';
autoUpdater.logger = log; autoUpdater.logger = log;
@@ -45,18 +46,63 @@ function buildMenu() {
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`); 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', label: 'New query',
click() { click() {
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`); 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', label: 'View',
submenu: [ submenu: [
@@ -71,20 +117,6 @@ function buildMenu() {
{ role: 'togglefullscreen' }, { role: 'togglefullscreen' },
], ],
}, },
{
role: 'window',
submenu: [
{
label: 'Close all tabs',
click() {
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
},
},
{ type: 'separator' },
{ role: 'minimize' },
{ role: 'close' },
],
},
{ {
role: 'help', role: 'help',
submenu: [ submenu: [
@@ -97,7 +129,7 @@ function buildMenu() {
{ {
label: 'DbGate on GitHub', label: 'DbGate on GitHub',
click() { 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'); 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', label: 'About',
click() { click() {
@@ -119,6 +157,12 @@ function buildMenu() {
return Menu.buildFromTemplate(template); 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() { function createWindow() {
const bounds = store.get('winBounds'); const bounds = store.get('winBounds');
@@ -135,7 +179,8 @@ function createWindow() {
}, },
}); });
mainWindow.setMenu(buildMenu()); mainMenu = buildMenu();
mainWindow.setMenu(mainMenu);
function loadMainWindow() { function loadMainWindow() {
const startUrl = const startUrl =

View File

@@ -717,10 +717,10 @@ electron-updater@^4.3.5:
lodash.isequal "^4.5.0" lodash.isequal "^4.5.0"
semver "^7.3.2" semver "^7.3.2"
electron@11.1.1: electron@11.2.1:
version "11.1.1" version "11.2.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-11.1.1.tgz#188f036f8282798398dca9513e9bb3b10213e3aa" resolved "https://registry.yarnpkg.com/electron/-/electron-11.2.1.tgz#8641dd1a62911a1144e0c73c34fd9f37ccc65c2b"
integrity sha512-tlbex3xosJgfileN6BAQRotevPRXB/wQIq48QeQ08tUJJrXwE72c8smsM/hbHx5eDgnbfJ2G3a60PmRjHU2NhA== integrity sha512-Im1y29Bnil+Nzs+FCTq01J1OtLbs+2ZGLLllaqX/9n5GgpdtDmZhS/++JHBsYZ+4+0n7asO+JKQgJD+CqPClzg==
dependencies: dependencies:
"@electron/get" "^1.0.1" "@electron/get" "^1.0.1"
"@types/node" "^12.0.12" "@types/node" "^12.0.12"

View File

@@ -78,6 +78,11 @@ module.exports = {
} }
}, },
saveAs_meta: 'post',
async saveAs({ filePath, data, format }) {
await fs.writeFile(filePath, serialize(format, data));
},
favorites_meta: 'get', favorites_meta: 'get',
async favorites() { async favorites() {
if (!hasPermission(`files/favorites/read`)) return []; if (!hasPermission(`files/favorites/read`)) return [];

View File

@@ -29,8 +29,8 @@ const hasPermission = require('../utility/hasPermission');
const preinstallPluginMinimalVersions = { const preinstallPluginMinimalVersions = {
'dbgate-plugin-mssql': '1.0.10', 'dbgate-plugin-mssql': '1.0.10',
'dbgate-plugin-mysql': '1.0.3', 'dbgate-plugin-mysql': '1.0.4',
'dbgate-plugin-postgres': '1.0.2', 'dbgate-plugin-postgres': '1.0.3',
'dbgate-plugin-csv': '1.0.8', 'dbgate-plugin-csv': '1.0.8',
'dbgate-plugin-excel': '1.0.6', 'dbgate-plugin-excel': '1.0.6',
}; };

View File

@@ -8,6 +8,7 @@ const lock = new AsyncLock();
module.exports = { module.exports = {
opened: [], opened: [],
closed: {}, closed: {},
lastPinged: {},
handle_databases(conid, { databases }) { handle_databases(conid, { databases }) {
const existing = this.opened.find(x => x.conid == conid); const existing = this.opened.find(x => x.conid == conid);
@@ -88,7 +89,12 @@ module.exports = {
ping_meta: 'post', ping_meta: 'post',
async ping({ connections }) { async ping({ connections }) {
await Promise.all( 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); const opened = await this.ensureOpened(conid);
opened.subprocess.send({ msgtype: 'ping' }); opened.subprocess.send({ msgtype: 'ping' });
}) })

View File

@@ -1,5 +1,5 @@
{ {
"version": "1.0.7", "version": "1.0.8",
"name": "dbgate-tools", "name": "dbgate-tools",
"main": "lib/index.js", "main": "lib/index.js",
"typings": "lib/index.d.ts", "typings": "lib/index.d.ts",

View File

@@ -49,3 +49,15 @@ export function findObjectLike(
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) { export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName)); 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);
}
}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { FontIcon } from './icons'; import { FontIcon } from './icons';
import useTheme from './theme/useTheme'; import useTheme from './theme/useTheme';
import getElectron from './utility/getElectron';
import useExtensions from './utility/useExtensions'; import useExtensions from './utility/useExtensions';
const TargetStyled = styled.div` const TargetStyled = styled.div`
@@ -41,6 +42,9 @@ const TitleWrapper = styled.div`
export default function DragAndDropFileTarget({ isDragActive, inputProps }) { export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
const theme = useTheme(); const theme = useTheme();
const { fileFormats } = useExtensions(); const { fileFormats } = useExtensions();
const electron = getElectron();
const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name);
if (electron) fileTypeNames.push('SQL');
return ( return (
!!isDragActive && ( !!isDragActive && (
<TargetStyled theme={theme}> <TargetStyled theme={theme}>
@@ -49,13 +53,7 @@ export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
<FontIcon icon="icon cloud-upload" /> <FontIcon icon="icon cloud-upload" />
</IconWrapper> </IconWrapper>
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper> <TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
<InfoWrapper> <InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
Supported file types:{' '}
{fileFormats
.filter(x => x.readerFunc)
.map(x => x.name)
.join(', ')}
</InfoWrapper>
</InfoBox> </InfoBox>
<input {...inputProps} /> <input {...inputProps} />
</TargetStyled> </TargetStyled>

View File

@@ -100,6 +100,7 @@ export default function Screen() {
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness ? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
: dimensions.widgetMenu.iconSize; : dimensions.widgetMenu.iconSize;
const toolbarPortalRef = React.useRef(); const toolbarPortalRef = React.useRef();
const statusbarPortalRef = React.useRef();
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff)); const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
const { getRootProps, getInputProps, isDragActive } = useUploadsZone(); const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
@@ -131,10 +132,10 @@ export default function Screen() {
<TabsPanel></TabsPanel> <TabsPanel></TabsPanel>
</TabsPanelContainer> </TabsPanelContainer>
<BodyDiv contentLeft={contentLeft} theme={theme}> <BodyDiv contentLeft={contentLeft} theme={theme}>
<TabContent toolbarPortalRef={toolbarPortalRef} /> <TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
</BodyDiv> </BodyDiv>
<StausBarContainer theme={theme}> <StausBarContainer theme={theme}>
<StatusBar /> <StatusBar statusbarPortalRef={statusbarPortalRef} />
</StausBarContainer> </StausBarContainer>
<ModalLayer /> <ModalLayer />
<MenuLayer /> <MenuLayer />

View File

@@ -18,12 +18,18 @@ const TabContainerStyled = styled.div`
`; `;
function TabContainer({ TabComponent, ...props }) { function TabContainer({ TabComponent, ...props }) {
const { tabVisible, tabid, toolbarPortalRef } = props; const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props;
return ( return (
// @ts-ignore // @ts-ignore
<TabContainerStyled tabVisible={tabVisible}> <TabContainerStyled tabVisible={tabVisible}>
<ErrorBoundary> <ErrorBoundary>
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} /> <TabComponent
{...props}
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
/>
</ErrorBoundary> </ErrorBoundary>
</TabContainerStyled> </TabContainerStyled>
); );
@@ -42,7 +48,7 @@ function createTabComponent(selectedTab) {
return null; return null;
} }
export default function TabContent({ toolbarPortalRef }) { export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) {
const files = useOpenedTabs(); const files = useOpenedTabs();
const [mountedTabs, setMountedTabs] = React.useState({}); const [mountedTabs, setMountedTabs] = React.useState({});
@@ -84,6 +90,7 @@ export default function TabContent({ toolbarPortalRef }) {
tabid={tabid} tabid={tabid}
tabVisible={tabVisible} tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef} toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
TabComponent={TabComponent} TabComponent={TabComponent}
/> />
); );

View File

@@ -10,6 +10,7 @@ import useTheme from './theme/useTheme';
import usePropsCompare from './utility/usePropsCompare'; import usePropsCompare from './utility/usePropsCompare';
import { useShowMenu } from './modals/showMenu'; import { useShowMenu } from './modals/showMenu';
import { setSelectedTabFunc } from './utility/common'; import { setSelectedTabFunc } from './utility/common';
import getElectron from './utility/getElectron';
// const files = [ // const files = [
// { name: 'app.js' }, // { name: 'app.js' },
@@ -124,6 +125,15 @@ function getDbIcon(key) {
return 'icon file'; 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() { export default function TabsPanel() {
// const formatDbKey = (conid, database) => `${database}-${conid}`; // const formatDbKey = (conid, database) => `${database}-${conid}`;
const theme = useTheme(); 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( // console.log(
// 't', // 't',
// tabs.map(x => x.tooltip) // tabs.map(x => x.tooltip)
@@ -252,7 +272,7 @@ export default function TabsPanel() {
{_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => ( {_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => (
<FileTabItem <FileTabItem
{...tab} {...tab}
title={tab.tooltip} title={buildTooltip(tab)}
key={tab.tabid} key={tab.tabid}
theme={theme} theme={theme}
onClick={e => handleTabClick(e, tab.tabid)} onClick={e => handleTabClick(e, tab.tabid)}

View File

@@ -49,6 +49,7 @@ export function AppObjectCore({
extInfo = undefined, extInfo = undefined,
statusTitle = undefined, statusTitle = undefined,
disableHover = false, disableHover = false,
children = null,
Menu = undefined, Menu = undefined,
...other ...other
}) { }) {
@@ -63,6 +64,7 @@ export function AppObjectCore({
}; };
return ( return (
<>
<AppObjectDiv <AppObjectDiv
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onClick={() => { onClick={() => {
@@ -89,5 +91,7 @@ export function AppObjectCore({
)} )}
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>} {extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
</AppObjectDiv> </AppObjectDiv>
{children}
</>
); );
} }

View File

@@ -5,6 +5,14 @@ import { DropDownMenuItem } from '../modals/DropDownMenu';
import { useSetOpenedTabs } from '../utility/globalState'; import { useSetOpenedTabs } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore'; import { AppObjectCore } from './AppObjectCore';
import { setSelectedTabFunc } from '../utility/common'; 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 }) { function Menu({ data }) {
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
@@ -25,18 +33,17 @@ function Menu({ data }) {
function ClosedTabAppObject({ data, commonProps }) { function ClosedTabAppObject({ data, commonProps }) {
const { tabid, props, selected, icon, title, closedTime, busy } = data; const { tabid, props, selected, icon, title, closedTime, busy } = data;
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const theme = useTheme();
const onClick = () => { const onClick = () => {
setOpenedTabs(files => setOpenedTabs(files =>
setSelectedTabFunc( setSelectedTabFunc(
files.map( files.map(x => ({
x => ({
...x, ...x,
closedTime: x.tabid == tabid ? undefined : x.closedTime, closedTime: x.tabid == tabid ? undefined : x.closedTime,
}), })),
tabid tabid
) )
)
); );
}; };
@@ -50,7 +57,14 @@ function ClosedTabAppObject({ data, commonProps }) {
onClick={onClick} onClick={onClick}
isBusy={busy} isBusy={busy}
Menu={Menu} Menu={Menu}
/> >
{data.props && data.props.database && (
<InfoDiv theme={theme}>
<FontIcon icon="icon database" /> {data.props.database}
</InfoDiv>
)}
{data.contentPreview && <InfoDiv theme={theme}>{data.contentPreview}</InfoDiv>}
</AppObjectCore>
); );
} }

View File

@@ -40,7 +40,7 @@ function Menu({ data }) {
setOpenedConnections(list => list.filter(x => x != data._id)); setOpenedConnections(list => list.filter(x => x != data._id));
}; };
const handleConnect = () => { const handleConnect = () => {
setOpenedConnections(list => [...list, data._id]); setOpenedConnections(list => _.uniq([...list, data._id]));
}; };
return ( return (
<> <>
@@ -72,7 +72,7 @@ function ConnectionAppObject({ data, commonProps }) {
const extensions = useExtensions(); const extensions = useExtensions();
const isBold = _.get(currentDatabase, 'connection._id') == _id; const isBold = _.get(currentDatabase, 'connection._id') == _id;
const onClick = () => setOpenedConnections(c => [...c, _id]); const onClick = () => setOpenedConnections(c => _.uniq([...c, _id]));
let statusIcon = null; let statusIcon = null;
let statusTitle = null; let statusTitle = null;

View File

@@ -20,7 +20,7 @@ function Menu({ data }) {
const handleNewQuery = () => { const handleNewQuery = () => {
openNewTab({ openNewTab({
title: 'Query', title: 'Query #',
icon: 'img sql-file', icon: 'img sql-file',
tooltip, tooltip,
tabComponent: 'QueryTab', tabComponent: 'QueryTab',

View File

@@ -149,7 +149,7 @@ export async function openDatabaseObjectDetail(
openNewTab( openNewTab(
{ {
title: pureName, title: sqlTemplate ? 'Query #' : pureName,
tooltip, tooltip,
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField], icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent, tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
@@ -245,7 +245,7 @@ function Menu({ data }) {
} else if (menu.isQueryDesigner) { } else if (menu.isQueryDesigner) {
openNewTab( openNewTab(
{ {
title: data.pureName, title: 'Query #',
icon: 'img query-design', icon: 'img query-design',
tabComponent: 'QueryDesignTab', tabComponent: 'QueryDesignTab',
props: { props: {

View File

@@ -104,7 +104,7 @@ export function SavedSqlFileAppObject({ data, commonProps }) {
openNewTab( openNewTab(
{ {
title: 'Shell', title: 'Shell #',
icon: 'img shell', icon: 'img shell',
tabComponent: 'ShellTab', tabComponent: 'ShellTab',
}, },

View File

@@ -1,17 +1,9 @@
import React from 'react'; import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; import ToolbarButton from '../widgets/ToolbarButton';
export default function ChartToolbar({ save, modelState, dispatchModel }) { export default function ChartToolbar({ modelState, dispatchModel }) {
const hasPermission = useHasPermission();
return ( return (
<> <>
{hasPermission('files/charts/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo"> <ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo Undo
</ToolbarButton> </ToolbarButton>

View File

@@ -312,7 +312,7 @@ export default function DataGridCore(props) {
} }
} }
: null, : null,
[formViewAvailable, display] [formViewAvailable, display, openNewTab]
); );
if (!columns || columns.length == 0) return <LoadingInfo wrapper message="Waiting for structure" />; if (!columns || columns.length == 0) return <LoadingInfo wrapper message="Waiting for structure" />;
@@ -353,7 +353,7 @@ export default function DataGridCore(props) {
const handleOpenFreeTable = () => { const handleOpenFreeTable = () => {
openNewTab( openNewTab(
{ {
title: 'selection', title: 'Data #',
icon: 'img free-table', icon: 'img free-table',
tabComponent: 'FreeTableTab', tabComponent: 'FreeTableTab',
props: {}, props: {},
@@ -365,7 +365,7 @@ export default function DataGridCore(props) {
const handleOpenChart = () => { const handleOpenChart = () => {
openNewTab( openNewTab(
{ {
title: 'Chart', title: 'Chart #',
icon: 'img chart', icon: 'img chart',
tabComponent: 'ChartTab', tabComponent: 'ChartTab',
props: {}, props: {},

View File

@@ -83,7 +83,7 @@ export default function SqlDataGridCore(props) {
function openActiveChart() { function openActiveChart() {
openNewTab( openNewTab(
{ {
title: 'Chart', title: 'Chart #',
icon: 'img chart', icon: 'img chart',
tabComponent: 'ChartTab', tabComponent: 'ChartTab',
props: { props: {
@@ -104,7 +104,7 @@ export default function SqlDataGridCore(props) {
function openQuery() { function openQuery() {
openNewTab( openNewTab(
{ {
title: 'Query', title: 'Query #',
icon: 'img sql-file', icon: 'img sql-file',
tabComponent: 'QueryTab', tabComponent: 'QueryTab',
props: { props: {

View File

@@ -1,18 +1,15 @@
import React from 'react'; import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; import ToolbarButton from '../widgets/ToolbarButton';
export default function QueryDesignToolbar({ export default function QueryDesignToolbar({
execute, execute,
isDatabaseDefined, isDatabaseDefined,
busy, busy,
save,
modelState, modelState,
dispatchModel, dispatchModel,
isConnected, isConnected,
kill, kill,
}) { }) {
const hasPermission = useHasPermission();
return ( return (
<> <>
<ToolbarButton disabled={!isDatabaseDefined || busy} onClick={execute} icon="icon run"> <ToolbarButton disabled={!isDatabaseDefined || busy} onClick={execute} icon="icon run">
@@ -21,11 +18,6 @@ export default function QueryDesignToolbar({
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close"> <ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
Kill Kill
</ToolbarButton> </ToolbarButton>
{hasPermission('files/query/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo"> <ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo Undo
</ToolbarButton> </ToolbarButton>

View File

@@ -15,7 +15,7 @@ const Container = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: #ddeeee; background: ${props => props.theme.gridheader_background_cyan[0]};
height: ${dimensions.toolBar.height}px; height: ${dimensions.toolBar.height}px;
min-height: ${dimensions.toolBar.height}px; min-height: ${dimensions.toolBar.height}px;
overflow: hidden; overflow: hidden;

View File

@@ -6,7 +6,7 @@ export default function useNewFreeTable() {
return ({ title = undefined, ...props } = {}) => return ({ title = undefined, ...props } = {}) =>
openNewTab({ openNewTab({
title: title || 'Table', title: title || 'Data #',
icon: 'img free-table', icon: 'img free-table',
tabComponent: 'FreeTableTab', tabComponent: 'FreeTableTab',
props, props,

View File

@@ -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 { values, setFieldValue, setValues } = useForm();
const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName }); const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName });
const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId }); const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId });
@@ -453,6 +457,21 @@ export default function ImportExportConfigurator({ uploadedFile = undefined, onC
if (uploadedFile) { if (uploadedFile) {
handleUpload(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 = const supportsPreview =

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import _ from 'lodash';
import './index.css'; import './index.css';
import '@mdi/font/css/materialdesignicons.css'; import '@mdi/font/css/materialdesignicons.css';
import App from './App'; import App from './App';
@@ -22,6 +23,17 @@ import localStorageGarbageCollector from './utility/localStorageGarbageCollector
// import 'ace-builds/src-noconflict/snippets/mysql'; // import 'ace-builds/src-noconflict/snippets/mysql';
localStorageGarbageCollector(); 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(<App />, document.getElementById('root')); ReactDOM.render(<App />, document.getElementById('root'));

View File

@@ -1,17 +1,9 @@
import React from 'react'; import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; import ToolbarButton from '../widgets/ToolbarButton';
export default function MarkdownToolbar({ save, showPreview }) { export default function MarkdownToolbar({ showPreview }) {
const hasPermission = useHasPermission();
return ( return (
<> <>
{hasPermission('files/markdown/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton onClick={showPreview} icon="icon preview"> <ToolbarButton onClick={showPreview} icon="icon preview">
Preview Preview
</ToolbarButton> </ToolbarButton>

View File

@@ -100,7 +100,7 @@ function GenerateSctriptButton({ modalState }) {
const code = await createImpExpScript(extensions, values); const code = await createImpExpScript(extensions, values);
openNewTab( openNewTab(
{ {
title: 'Shell', title: 'Shell #',
icon: 'img shell', icon: 'img shell',
tabComponent: 'ShellTab', tabComponent: 'ShellTab',
}, },
@@ -120,6 +120,7 @@ export default function ImportExportModal({
modalState, modalState,
initialValues, initialValues,
uploadedFile = undefined, uploadedFile = undefined,
openedFile = undefined,
importToArchive = false, importToArchive = false,
}) { }) {
const [executeNumber, setExecuteNumber] = React.useState(0); const [executeNumber, setExecuteNumber] = React.useState(0);
@@ -195,7 +196,11 @@ export default function ImportExportModal({
<ModalHeader modalState={modalState}>Import/Export {busy && <FontIcon icon="icon loading" />}</ModalHeader> <ModalHeader modalState={modalState}>Import/Export {busy && <FontIcon icon="icon loading" />}</ModalHeader>
<Wrapper> <Wrapper>
<ContentWrapper theme={theme}> <ContentWrapper theme={theme}>
<ImportExportConfigurator uploadedFile={uploadedFile} onChangePreview={setPreviewReader} /> <ImportExportConfigurator
uploadedFile={uploadedFile}
openedFile={openedFile}
onChangePreview={setPreviewReader}
/>
</ContentWrapper> </ContentWrapper>
<WidgetColumnWrapper theme={theme}> <WidgetColumnWrapper theme={theme}>
<WidgetColumnBar> <WidgetColumnBar>

View File

@@ -6,14 +6,51 @@ import ModalHeader from './ModalHeader';
import ModalContent from './ModalContent'; import ModalContent from './ModalContent';
import ModalFooter from './ModalFooter'; import ModalFooter from './ModalFooter';
import { FormProvider } from '../utility/FormProvider'; 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 handleSubmit = async values => {
const { name } = values; const { name } = values;
await axios.post('files/save', { folder, file: name, data, format }); await axios.post('files/save', { folder, file: name, data, format });
modalState.close(); 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 ( return (
<ModalBase modalState={modalState}> <ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Save file</ModalHeader> <ModalHeader modalState={modalState}>Save file</ModalHeader>
@@ -23,6 +60,25 @@ export default function SaveFileModal({ data, folder, format, modalState, name,
</ModalContent> </ModalContent>
<ModalFooter> <ModalFooter>
<FormSubmit value="Save" onClick={handleSubmit} /> <FormSubmit value="Save" onClick={handleSubmit} />
{electron && (
<FormStyledButton
type="button"
value="Save to disk"
onClick={() => {
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);
}
}}
/>
)}
</ModalFooter> </ModalFooter>
</FormProvider> </FormProvider>
</ModalBase> </ModalBase>

View File

@@ -1,53 +1,117 @@
import React from 'react'; import React from 'react';
import axios from '../utility/axios';
import { changeTab } from '../utility/common'; import { changeTab } from '../utility/common';
import getElectron from '../utility/getElectron';
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState'; import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
import keycodes from '../utility/keycodes'; 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 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 setOpenedTabs = useSetOpenedTabs();
const openedTabs = useOpenedTabs(); 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 { savedFile, savedFilePath } = openedTabs.find(x => x.tabid == tabid).props || {};
const onSave = name => const onSave = (title, newProps) => {
changeTab(tabid, setOpenedTabs, tab => ({ changeTab(tabid, setOpenedTabs, tab => ({
...tab, ...tab,
title: name, title,
props: { props: {
...tab.props, ...tab.props,
savedFile: name,
savedFolder: folder,
savedFormat: format, 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( const handleKeyboard = React.useCallback(
e => { e => {
if (e.keyCode == keycodes.s && e.ctrlKey) { if (e.keyCode == keycodes.s && e.ctrlKey) {
e.preventDefault(); e.preventDefault();
modalState.open(); if (e.shiftKey) {
saveFileModalState.open();
} else {
if (savedFile || savedFilePath) handleSaveRef.current();
else saveFileModalState.open();
}
} }
}, },
[modalState] [saveFileModalState]
); );
React.useEffect(() => { React.useEffect(() => {
if (tabVisible) { if (tabVisible && canSave) {
document.addEventListener('keydown', handleKeyboard); document.addEventListener('keydown', handleKeyboard);
return () => { return () => {
document.removeEventListener('keydown', handleKeyboard); 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 ( return (
<>
<SaveFileModal <SaveFileModal
data={data} data={data}
folder={folder} folder={folder}
format={format} format={format}
modalState={modalState} modalState={saveFileModalState}
name={savedFile || 'newFile'} name={savedFile || 'newFile'}
filePath={savedFilePath}
fileExtension={fileExtension}
onSave={onSave} onSave={onSave}
/> />
{canSave && (
<ToolbarPortal tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef}>
<SaveFileToolbarButton
saveAs={saveFileModalState.open}
save={savedFile || savedFilePath ? handleSave : null}
tabid={tabid}
/>
</ToolbarPortal>
)}
</>
); );
} }

View File

@@ -2,7 +2,7 @@ import React from 'react';
import useHasPermission from '../utility/useHasPermission'; import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; 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(); const hasPermission = useHasPermission();
return ( return (
<> <>
@@ -15,11 +15,11 @@ export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, f
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close"> <ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
Kill Kill
</ToolbarButton> </ToolbarButton>
{hasPermission('files/sql/write') && ( {/* {hasPermission('files/sql/write') && (
<ToolbarButton onClick={save} icon="icon save"> <ToolbarButton onClick={save} icon="icon save">
Save Save
</ToolbarButton> </ToolbarButton>
)} )} */}
<ToolbarButton onClick={format} icon="icon format-code"> <ToolbarButton onClick={format} icon="icon format-code">
Format Format
</ToolbarButton> </ToolbarButton>

View File

@@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton'; import ToolbarButton from '../widgets/ToolbarButton';
export default function ShellToolbar({ execute, cancel, busy, edit, save, editAvailable }) { export default function ShellToolbar({ execute, cancel, busy, edit, editAvailable }) {
const hasPermission = useHasPermission();
return ( return (
<> <>
<ToolbarButton disabled={busy} onClick={execute} icon="icon run"> <ToolbarButton disabled={busy} onClick={execute} icon="icon run">
@@ -15,11 +13,6 @@ export default function ShellToolbar({ execute, cancel, busy, edit, save, editAv
<ToolbarButton disabled={!editAvailable} onClick={edit} icon="icon show-wizard"> <ToolbarButton disabled={!editAvailable} onClick={edit} icon="icon show-wizard">
Show wizard Show wizard
</ToolbarButton> </ToolbarButton>
{hasPermission('files/shell/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
</> </>
); );
} }

View File

@@ -14,7 +14,7 @@ export default function useNewQuery() {
return ({ title = undefined, initialData = undefined, ...props } = {}) => return ({ title = undefined, initialData = undefined, ...props } = {}) =>
openNewTab( openNewTab(
{ {
title: title || 'Query', title: title || 'Query #',
icon: 'img sql-file', icon: 'img sql-file',
tooltip, tooltip,
tabComponent: 'QueryTab', tabComponent: 'QueryTab',
@@ -40,7 +40,7 @@ export function useNewQueryDesign() {
return ({ title = undefined, initialData = undefined, ...props } = {}) => return ({ title = undefined, initialData = undefined, ...props } = {}) =>
openNewTab( openNewTab(
{ {
title: title || 'Query', title: title || 'Query #',
icon: 'img query-design', icon: 'img query-design',
tooltip, tooltip,
tabComponent: 'QueryDesignTab', tabComponent: 'QueryDesignTab',

View File

@@ -4,17 +4,16 @@ import { createFreeTableModel } from 'dbgate-datalib';
import useUndoReducer from '../utility/useUndoReducer'; import useUndoReducer from '../utility/useUndoReducer';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useUpdateDatabaseForTab } from '../utility/globalState'; import { useUpdateDatabaseForTab } from '../utility/globalState';
import useModalState from '../modals/useModalState';
import LoadingInfo from '../widgets/LoadingInfo'; import LoadingInfo from '../widgets/LoadingInfo';
import ErrorInfo from '../widgets/ErrorInfo'; import ErrorInfo from '../widgets/ErrorInfo';
import useEditorData from '../utility/useEditorData'; import useEditorData from '../utility/useEditorData';
import SaveTabModal from '../modals/SaveTabModal'; import SaveTabModal from '../modals/SaveTabModal';
import ChartEditor from '../charts/ChartEditor'; import ChartEditor from '../charts/ChartEditor';
import ChartToolbar from '../charts/ChartToolbar'; import ChartToolbar from '../charts/ChartToolbar';
import ToolbarPortal from '../utility/ToolbarPortal';
export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database, tabid }) { export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database, tabid }) {
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel()); const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
const saveFileModalState = useModalState();
const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({ const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({
tabid, tabid,
}); });
@@ -57,20 +56,17 @@ export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database
database={database} database={database}
/> />
<SaveTabModal <SaveTabModal
modalState={saveFileModalState}
tabVisible={tabVisible} tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={modelState.value} data={modelState.value}
format="json" format="json"
folder="charts" folder="charts"
tabid={tabid} tabid={tabid}
fileExtension="chart"
/> />
{toolbarPortalRef && <ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
toolbarPortalRef.current && <ChartToolbar modelState={modelState} dispatchModel={dispatchModel} />
tabVisible && </ToolbarPortal>
ReactDOM.createPortal(
<ChartToolbar save={saveFileModalState.open} modelState={modelState} dispatchModel={dispatchModel} />,
toolbarPortalRef.current
)}
</> </>
); );
} }

View File

@@ -15,7 +15,7 @@ import useEditorData from '../utility/useEditorData';
export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, toolbarPortalRef, tabid, initialArgs }) { export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, toolbarPortalRef, tabid, initialArgs }) {
const [config, setConfig] = useGridConfig(tabid); const [config, setConfig] = useGridConfig(tabid);
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel()); const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
const saveFileModalState = useModalState(); const saveArchiveModalState = useModalState();
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({ const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({
tabid, tabid,
@@ -59,9 +59,9 @@ export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, to
dispatchModel={dispatchModel} dispatchModel={dispatchModel}
tabVisible={tabVisible} tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef} toolbarPortalRef={toolbarPortalRef}
onSave={() => saveFileModalState.open()} onSave={() => saveArchiveModalState.open()}
/> />
<SaveArchiveModal modalState={saveFileModalState} folder={archiveFolder} file={archiveFile} onSave={handleSave} /> <SaveArchiveModal modalState={saveArchiveModalState} folder={archiveFolder} file={archiveFile} onSave={handleSave} />
</> </>
); );
} }

View File

@@ -11,10 +11,10 @@ import LoadingInfo from '../widgets/LoadingInfo';
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState'; import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
import useOpenNewTab from '../utility/useOpenNewTab'; import useOpenNewTab from '../utility/useOpenNewTab';
import { setSelectedTabFunc } from '../utility/common'; import { setSelectedTabFunc } from '../utility/common';
import ToolbarPortal from '../utility/ToolbarPortal';
export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) { export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) {
const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid }); const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid });
const saveFileModalState = useModalState();
const openedTabs = useOpenedTabs(); const openedTabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const openNewTab = useOpenNewTab(); const openNewTab = useOpenNewTab();
@@ -61,20 +61,17 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef,
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
mode="markdown" mode="markdown"
/> />
{toolbarPortalRef && <ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
toolbarPortalRef.current && <MarkdownToolbar showPreview={showPreview} />
tabVisible && </ToolbarPortal>
ReactDOM.createPortal(
<MarkdownToolbar save={saveFileModalState.open} showPreview={showPreview} />,
toolbarPortalRef.current
)}
<SaveTabModal <SaveTabModal
modalState={saveFileModalState}
tabVisible={tabVisible} tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={editorData} data={editorData}
format="text" format="text"
folder="markdown" folder="markdown"
tabid={tabid} tabid={tabid}
fileExtension="md"
/> />
</> </>
); );

View File

@@ -15,7 +15,6 @@ import keycodes from '../utility/keycodes';
import { changeTab } from '../utility/common'; import { changeTab } from '../utility/common';
import useSocket from '../utility/SocketProvider'; import useSocket from '../utility/SocketProvider';
import SaveTabModal from '../modals/SaveTabModal'; import SaveTabModal from '../modals/SaveTabModal';
import useModalState from '../modals/useModalState';
import sqlFormatter from 'sql-formatter'; import sqlFormatter from 'sql-formatter';
import useEditorData from '../utility/useEditorData'; import useEditorData from '../utility/useEditorData';
import LoadingInfo from '../widgets/LoadingInfo'; import LoadingInfo from '../widgets/LoadingInfo';
@@ -25,15 +24,25 @@ import QueryDesignColumns from '../designer/QueryDesignColumns';
import { findEngineDriver } from 'dbgate-tools'; import { findEngineDriver } from 'dbgate-tools';
import { generateDesignedQuery } from '../designer/designerTools'; import { generateDesignedQuery } from '../designer/designerTools';
import useUndoReducer from '../utility/useUndoReducer'; 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 [sessionId, setSessionId] = React.useState(null);
const [visibleResultTabs, setVisibleResultTabs] = React.useState(false); const [visibleResultTabs, setVisibleResultTabs] = React.useState(false);
const [executeNumber, setExecuteNumber] = React.useState(0); const [executeNumber, setExecuteNumber] = React.useState(0);
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const socket = useSocket(); const socket = useSocket();
const [busy, setBusy] = React.useState(false); const [busy, setBusy] = React.useState(false);
const saveFileModalState = useModalState();
const extensions = useExtensions(); const extensions = useExtensions();
const connection = useConnectionInfo({ conid }); const connection = useConnectionInfo({ conid });
const engine = findEngineDriver(connection, extensions); const engine = findEngineDriver(connection, extensions);
@@ -49,6 +58,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
}, },
{ mergeNearActions: true } { mergeNearActions: true }
); );
const timerLabel = useTimerLabel();
React.useEffect(() => { React.useEffect(() => {
// @ts-ignore // @ts-ignore
@@ -61,6 +71,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
const handleSessionDone = React.useCallback(() => { const handleSessionDone = React.useCallback(() => {
setBusy(false); setBusy(false);
timerLabel.stop();
}, []); }, []);
const generatePreview = (value, engine) => { const generatePreview = (value, engine) => {
@@ -114,6 +125,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
setSessionId(sesid); setSessionId(sesid);
} }
setBusy(true); setBusy(true);
timerLabel.start();
await axios.post('sessions/execute-query', { await axios.post('sessions/execute-query', {
sesid, sesid,
sql: sqlPreview, sql: sqlPreview,
@@ -126,6 +138,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
}); });
setSessionId(null); setSessionId(null);
setBusy(false); setBusy(false);
timerLabel.stop();
}; };
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
@@ -182,10 +195,7 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
)} )}
</ResultTabs> </ResultTabs>
</VerticalSplitter> </VerticalSplitter>
{toolbarPortalRef && <ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<QueryDesignToolbar <QueryDesignToolbar
modelState={modelState} modelState={modelState}
dispatchModel={dispatchModel} dispatchModel={dispatchModel}
@@ -194,19 +204,23 @@ export default function QueryDesignTab({ tabid, conid, database, tabVisible, too
busy={busy} busy={busy}
// cancel={handleCancel} // cancel={handleCancel}
// format={handleFormatCode} // format={handleFormatCode}
save={saveFileModalState.open}
isConnected={!!sessionId} isConnected={!!sessionId}
kill={handleKill} kill={handleKill}
/>, />
toolbarPortalRef.current </ToolbarPortal>
)} {statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<SaveTabModal <SaveTabModal
modalState={saveFileModalState} // modalState={saveFileModalState}
tabVisible={tabVisible} tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={modelState.value} data={modelState.value}
format="json" format="json"
folder="query" folder="query"
tabid={tabid} tabid={tabid}
fileExtension="qdesign"
/> />
</> </>
); );

View File

@@ -21,16 +21,46 @@ import useEditorData from '../utility/useEditorData';
import applySqlTemplate from '../utility/applySqlTemplate'; import applySqlTemplate from '../utility/applySqlTemplate';
import LoadingInfo from '../widgets/LoadingInfo'; import LoadingInfo from '../widgets/LoadingInfo';
import useExtensions from '../utility/useExtensions'; 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 [sessionId, setSessionId] = React.useState(null);
const [visibleResultTabs, setVisibleResultTabs] = React.useState(false); const [visibleResultTabs, setVisibleResultTabs] = React.useState(false);
const [executeNumber, setExecuteNumber] = React.useState(0); const [executeNumber, setExecuteNumber] = React.useState(0);
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const socket = useSocket(); const socket = useSocket();
const [busy, setBusy] = React.useState(false); const [busy, setBusy] = React.useState(false);
const saveFileModalState = useModalState();
const extensions = useExtensions(); const extensions = useExtensions();
const timerLabel = useTimerLabel();
const { editorData, setEditorData, isLoading } = useEditorData({ const { editorData, setEditorData, isLoading } = useEditorData({
tabid, tabid,
loadFromArgs: loadFromArgs:
@@ -43,6 +73,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
const handleSessionDone = React.useCallback(() => { const handleSessionDone = React.useCallback(() => {
setBusy(false); setBusy(false);
timerLabel.stop();
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@@ -61,6 +92,23 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
useUpdateDatabaseForTab(tabVisible, conid, database); useUpdateDatabaseForTab(tabVisible, conid, database);
const connection = useConnectionInfo({ conid }); 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 () => { const handleExecute = async () => {
if (busy) return; if (busy) return;
setExecuteNumber(num => num + 1); setExecuteNumber(num => num + 1);
@@ -77,6 +125,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
setSessionId(sesid); setSessionId(sesid);
} }
setBusy(true); setBusy(true);
timerLabel.start();
await axios.post('sessions/execute-query', { await axios.post('sessions/execute-query', {
sesid, sesid,
sql: selectedText || editorData, sql: selectedText || editorData,
@@ -95,6 +144,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
}); });
setSessionId(null); setSessionId(null);
setBusy(false); setBusy(false);
timerLabel.stop();
}; };
const handleKeyDown = (data, hash, keyString, keyCode, event) => { const handleKeyDown = (data, hash, keyString, keyCode, event) => {
@@ -151,7 +201,7 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
</ResultTabs> </ResultTabs>
)} )}
</VerticalSplitter> </VerticalSplitter>
{toolbarPortalRef && {/* {toolbarPortalRef &&
toolbarPortalRef.current && toolbarPortalRef.current &&
tabVisible && tabVisible &&
ReactDOM.createPortal( ReactDOM.createPortal(
@@ -166,14 +216,31 @@ export default function QueryTab({ tabid, conid, database, initialArgs, tabVisib
kill={handleKill} kill={handleKill}
/>, />,
toolbarPortalRef.current toolbarPortalRef.current
)} )} */}
{statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<QueryToolbar
isDatabaseDefined={conid && database}
execute={handleExecute}
busy={busy}
// cancel={handleCancel}
format={handleFormatCode}
// save={saveFileModalState.open}
isConnected={!!sessionId}
kill={handleKill}
/>
</ToolbarPortal>
<SaveTabModal <SaveTabModal
modalState={saveFileModalState} toolbarPortalRef={toolbarPortalRef}
tabVisible={tabVisible} tabVisible={tabVisible}
data={editorData} data={editorData}
format="text" format="text"
folder="sql" folder="sql"
tabid={tabid} tabid={tabid}
fileExtension="sql"
/> />
</> </>
); );

View File

@@ -14,18 +14,20 @@ import useShowModal from '../modals/showModal';
import ImportExportModal from '../modals/ImportExportModal'; import ImportExportModal from '../modals/ImportExportModal';
import useEditorData from '../utility/useEditorData'; import useEditorData from '../utility/useEditorData';
import SaveTabModal from '../modals/SaveTabModal'; import SaveTabModal from '../modals/SaveTabModal';
import useModalState from '../modals/useModalState';
import LoadingInfo from '../widgets/LoadingInfo'; 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 configRegex = /\s*\/\/\s*@ImportExportConfigurator\s*\n\s*\/\/\s*(\{[^\n]+\})\n/;
const requireRegex = /\s*(\/\/\s*@require\s+[^\n]+)\n/g; const requireRegex = /\s*(\/\/\s*@require\s+[^\n]+)\n/g;
const initRegex = /([^\n]+\/\/\s*@init)/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 [busy, setBusy] = React.useState(false);
const showModal = useShowModal(); const showModal = useShowModal();
const { editorData, setEditorData, isLoading } = useEditorData({ tabid }); const { editorData, setEditorData, isLoading } = useEditorData({ tabid });
const saveFileModalState = useModalState(); const timerLabel = useTimerLabel();
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
@@ -42,6 +44,7 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other
const handleRunnerDone = React.useCallback(() => { const handleRunnerDone = React.useCallback(() => {
setBusy(false); setBusy(false);
timerLabel.stop();
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@@ -69,12 +72,14 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other
runid = resp.data.runid; runid = resp.data.runid;
setRunnerId(runid); setRunnerId(runid);
setBusy(true); setBusy(true);
timerLabel.start();
}; };
const handleCancel = () => { const handleCancel = () => {
axios.post('runners/cancel', { axios.post('runners/cancel', {
runid: runnerId, runid: runnerId,
}); });
timerLabel.stop();
}; };
const handleKeyDown = (data, hash, keyString, keyCode, event) => { const handleKeyDown = (data, hash, keyString, keyCode, event) => {
@@ -114,27 +119,27 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, ...other
/> />
<RunnerOutputPane runnerId={runnerId} executeNumber={executeNumber} /> <RunnerOutputPane runnerId={runnerId} executeNumber={executeNumber} />
</VerticalSplitter> </VerticalSplitter>
{toolbarPortalRef && <ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<ShellToolbar <ShellToolbar
execute={handleExecute} execute={handleExecute}
busy={busy} busy={busy}
cancel={handleCancel} cancel={handleCancel}
edit={handleEdit} edit={handleEdit}
editAvailable={configRegex.test(editorData || '')} editAvailable={configRegex.test(editorData || '')}
save={saveFileModalState.open} />
/>, </ToolbarPortal>
toolbarPortalRef.current {statusbarPortalRef &&
)} statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<SaveTabModal <SaveTabModal
modalState={saveFileModalState} toolbarPortalRef={toolbarPortalRef}
tabVisible={tabVisible} tabVisible={tabVisible}
data={editorData} data={editorData}
format="text" format="text"
folder="shell" folder="shell"
tabid={tabid} tabid={tabid}
fileExtension="js"
/> />
</> </>
); );

View File

@@ -7,9 +7,11 @@ export default function ConnectionsPinger({ children }) {
const openedConnections = useOpenedConnections(); const openedConnections = useOpenedConnections();
const currentDatabase = useCurrentDatabase(); const currentDatabase = useCurrentDatabase();
const doPing = () => { const doServerPing = () => {
axios.post('server-connections/ping', { connections: openedConnections }); axios.post('server-connections/ping', { connections: openedConnections });
};
const doDatabasePing = () => {
const database = _.get(currentDatabase, 'name'); const database = _.get(currentDatabase, 'name');
const conid = _.get(currentDatabase, 'connection._id'); const conid = _.get(currentDatabase, 'connection._id');
if (conid && database) { if (conid && database) {
@@ -18,9 +20,16 @@ export default function ConnectionsPinger({ children }) {
}; };
React.useEffect(() => { React.useEffect(() => {
doPing(); doServerPing();
const handle = window.setInterval(doPing, 30 * 1000); const handle = window.setInterval(doServerPing, 30 * 1000);
return () => window.clearInterval(handle); return () => window.clearInterval(handle);
}, [openedConnections, currentDatabase]); }, [openedConnections]);
React.useEffect(() => {
doDatabasePing();
const handle = window.setInterval(doDatabasePing, 30 * 1000);
return () => window.clearInterval(handle);
}, [currentDatabase]);
return children; return children;
} }

View File

@@ -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 (
<ToolbarDropDownButton icon="icon save" text="Save">
<DropDownMenuItem onClick={save} keyText="Ctrl+S">
Save
</DropDownMenuItem>
<DropDownMenuItem onClick={saveAs} keyText="Ctrl+Shift+S">
Save As
</DropDownMenuItem>
</ToolbarDropDownButton>
);
}
return (
<ToolbarButton onClick={saveAs} icon="icon save">
Save As
</ToolbarButton>
);
}

View File

@@ -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
);
}

View File

@@ -3,8 +3,10 @@ import { useDropzone } from 'react-dropzone';
import ImportExportModal from '../modals/ImportExportModal'; import ImportExportModal from '../modals/ImportExportModal';
import useShowModal from '../modals/showModal'; import useShowModal from '../modals/showModal';
import { findFileFormat } from './fileformats'; import { findFileFormat } from './fileformats';
import getElectron from './getElectron';
import resolveApi from './resolveApi'; import resolveApi from './resolveApi';
import useExtensions from './useExtensions'; import useExtensions from './useExtensions';
import { useOpenElectronFileCore, canOpenByElectron } from './useOpenElectronFile';
const UploadsContext = React.createContext(null); const UploadsContext = React.createContext(null);
@@ -21,6 +23,8 @@ export function useUploadFiles() {
const { uploadListener } = useUploadsProvider(); const { uploadListener } = useUploadsProvider();
const showModal = useShowModal(); const showModal = useShowModal();
const extensions = useExtensions(); const extensions = useExtensions();
const electron = getElectron();
const openElectronFileCore = useOpenElectronFileCore();
const handleUploadFiles = React.useCallback( const handleUploadFiles = React.useCallback(
files => { files => {
@@ -31,6 +35,12 @@ export function useUploadFiles() {
} }
console.log('FILE', file); console.log('FILE', file);
if (electron && canOpenByElectron(file.path, extensions)) {
openElectronFileCore(file.path);
return;
}
const formData = new FormData(); const formData = new FormData();
formData.append('data', file); formData.append('data', file);

View File

@@ -43,6 +43,8 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
setValue(init); setValue(init);
valueRef.current = init; valueRef.current = init;
initialDataRef.current = init; initialDataRef.current = init;
// mark as not saved
changeCounterRef.current += 1;
} catch (err) { } catch (err) {
const message = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed'; const message = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed';
setErrorMessage(message); setErrorMessage(message);

View File

@@ -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 => (
<ImportExportModal
openedFile={{
filePath,
storageType: format.storageType,
shortName: parsed.name,
}}
modalState={modalState}
importToArchive
initialValues={{
sourceStorageType: format.storageType,
}}
/>
));
}
}
};
}
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);
}
};
}

View File

@@ -7,6 +7,14 @@ import { useOpenedTabs, useSetOpenedTabs } from './globalState';
import tabs from '../tabs'; import tabs from '../tabs';
import { setSelectedTabFunc } from './common'; 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() { export default function useOpenNewTab() {
const setOpenedTabs = useSetOpenedTabs(); const setOpenedTabs = useSetOpenedTabs();
const openedTabs = useOpenedTabs(); const openedTabs = useOpenedTabs();
@@ -15,11 +23,16 @@ export default function useOpenNewTab() {
async (newTab, initialData = undefined, options) => { async (newTab, initialData = undefined, options) => {
let existing = null; let existing = null;
const { savedFile } = newTab.props || {}; const { savedFile, savedFolder, savedFilePath } = newTab.props || {};
if (savedFile) { if (savedFile || savedFilePath) {
existing = openedTabs.find( existing = openedTabs.find(
x => 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; 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(); const tabid = uuidv1();
if (initialData) { if (initialData) {
for (const key of _.keys(initialData)) { for (const key of _.keys(initialData)) {
@@ -61,7 +83,7 @@ export default function useOpenNewTab() {
}, },
]); ]);
}, },
[setOpenedTabs] [setOpenedTabs, openedTabs]
); );
return openNewTab; return openNewTab;

View File

@@ -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 ... // Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage. // ... persists the new value to localStorage.
const setValue = value => { const setValue = React.useCallback(value => {
try { try {
// Allow value to be a function so we have same API as useState // 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 // Save state
setStoredValue(valueToStore); setStoredValue(valueToStore);
// Save to local storage // Save to local storage
@@ -31,7 +35,7 @@ export default function useStorage(key, storageObject, initialValue) {
console.error(error); console.error(error);
console.log('Error saving storage value', key, value); console.log('Error saving storage value', key, value);
} }
}; }, []);
return [storedValue, setValue]; return [storedValue, setValue];
} }

View File

@@ -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,
};
}

View File

@@ -44,14 +44,14 @@ export default function FavoritesWidget() {
const hasPermission = useHasPermission(); const hasPermission = useHasPermission();
return ( return (
<WidgetColumnBar> <WidgetColumnBar>
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs" height="20%">
<ClosedTabsList />
</WidgetColumnBarItem>
{hasPermission('files/favorites/read') && ( {hasPermission('files/favorites/read') && (
<WidgetColumnBarItem title="Favorites" name="favorites" height="15%"> <WidgetColumnBarItem title="Favorites" name="favorites" height="20%">
<FavoritesList /> <FavoritesList />
</WidgetColumnBarItem> </WidgetColumnBarItem>
)} )}
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs">
<ClosedTabsList />
</WidgetColumnBarItem>
</WidgetColumnBar> </WidgetColumnBar>
); );
} }

View File

@@ -10,12 +10,11 @@ const Container = styled.div`
display: flex; display: flex;
color: ${props => props.theme.statusbar_font1}; color: ${props => props.theme.statusbar_font1};
align-items: stretch; align-items: stretch;
justify-content: space-between;
`; `;
const Item = styled.div` export const StatusBarItem = styled.div`
padding: 2px 10px; padding: 2px 10px;
// margin: auto;
// flex-grow: 0;
`; `;
const ErrorWrapper = styled.span` const ErrorWrapper = styled.span`
@@ -30,32 +29,37 @@ const InfoWrapper = styled.span`
props.theme.statusbar_font_green[5]}; 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 { name, connection } = useCurrentDatabase() || {};
const status = useDatabaseStatus(connection ? { conid: connection._id, database: name } : {}); const status = useDatabaseStatus(connection ? { conid: connection._id, database: name } : {});
const { displayName, server, user, engine } = connection || {}; const { displayName, server, user, engine } = connection || {};
const theme = useTheme(); const theme = useTheme();
return ( return (
<Container theme={theme}> <Container theme={theme}>
<StatusbarContainer>
{name && ( {name && (
<Item> <StatusBarItem>
<FontIcon icon="icon database" /> {name} <FontIcon icon="icon database" /> {name}
</Item> </StatusBarItem>
)} )}
{(displayName || server) && ( {(displayName || server) && (
<Item> <StatusBarItem>
<FontIcon icon="icon server" /> {displayName || server} <FontIcon icon="icon server" /> {displayName || server}
</Item> </StatusBarItem>
)} )}
{user && ( {user && (
<Item> <StatusBarItem>
<FontIcon icon="icon account" /> {user} <FontIcon icon="icon account" /> {user}
</Item> </StatusBarItem>
)} )}
{connection && status && ( {connection && status && (
<Item> <StatusBarItem>
{status.name == 'pending' && ( {status.name == 'pending' && (
<> <>
<FontIcon icon="icon loading" /> Loading <FontIcon icon="icon loading" /> Loading
@@ -77,15 +81,17 @@ export default function StatusBar() {
Error Error
</> </>
)} )}
</Item> </StatusBarItem>
)} )}
{!connection && ( {!connection && (
<Item> <StatusBarItem>
<> <>
<FontIcon icon="icon disconnected" /> Not connected <FontIcon icon="icon disconnected" /> Not connected
</> </>
</Item> </StatusBarItem>
)} )}
</StatusbarContainer>
<StatusbarContainer ref={statusbarPortalRef}></StatusbarContainer>
</Container> </Container>
); );
} }

View File

@@ -25,6 +25,7 @@ import tabs from '../tabs';
import FavoriteModal from '../modals/FavoriteModal'; import FavoriteModal from '../modals/FavoriteModal';
import { useOpenFavorite } from '../appobj/FavoriteFileAppObject'; import { useOpenFavorite } from '../appobj/FavoriteFileAppObject';
import ErrorMessageModal from '../modals/ErrorMessageModal'; import ErrorMessageModal from '../modals/ErrorMessageModal';
import useOpenElectronFile from '../utility/useOpenElectronFile';
const ToolbarContainer = styled.div` const ToolbarContainer = styled.div`
display: flex; display: flex;
@@ -48,6 +49,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const electron = getElectron(); const electron = getElectron();
const favorites = useFavorites(); const favorites = useFavorites();
const openFavorite = useOpenFavorite(); const openFavorite = useOpenFavorite();
const openElectronFile = useOpenElectronFile();
const currentTab = openedTabs.find(x => x.selected); const currentTab = openedTabs.find(x => x.selected);
@@ -58,6 +60,7 @@ export default function ToolBar({ toolbarPortalRef }) {
window['dbgate_newQuery'] = newQuery; window['dbgate_newQuery'] = newQuery;
window['dbgate_closeAll'] = () => setOpenedTabs([]); window['dbgate_closeAll'] = () => setOpenedTabs([]);
window['dbgate_showAbout'] = showAbout; window['dbgate_showAbout'] = showAbout;
window['dbgate_openFile'] = openElectronFile;
}); });
const showAbout = () => { const showAbout = () => {
@@ -91,7 +94,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const newMarkdown = () => { const newMarkdown = () => {
openNewTab({ openNewTab({
title: 'Page', title: 'Page #',
tabComponent: 'MarkdownEditorTab', tabComponent: 'MarkdownEditorTab',
icon: 'img markdown', icon: 'img markdown',
}); });
@@ -103,7 +106,7 @@ export default function ToolBar({ toolbarPortalRef }) {
const newShell = () => { const newShell = () => {
openNewTab({ openNewTab({
title: 'Shell', title: 'Shell #',
icon: 'img shell', icon: 'img shell',
tabComponent: 'ShellTab', tabComponent: 'ShellTab',
}); });