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",
"version": "3.9.3",
"private": true,
"version": "3.9.4-beta.5",
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
"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": {

View File

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

View File

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

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',
async favorites() {
if (!hasPermission(`files/favorites/read`)) return [];

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 && (
<TargetStyled theme={theme}>
@@ -49,13 +53,7 @@ export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
<FontIcon icon="icon cloud-upload" />
</IconWrapper>
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
<InfoWrapper>
Supported file types:{' '}
{fileFormats
.filter(x => x.readerFunc)
.map(x => x.name)
.join(', ')}
</InfoWrapper>
<InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
</InfoBox>
<input {...inputProps} />
</TargetStyled>

View File

@@ -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() {
<TabsPanel></TabsPanel>
</TabsPanelContainer>
<BodyDiv contentLeft={contentLeft} theme={theme}>
<TabContent toolbarPortalRef={toolbarPortalRef} />
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
</BodyDiv>
<StausBarContainer theme={theme}>
<StatusBar />
<StatusBar statusbarPortalRef={statusbarPortalRef} />
</StausBarContainer>
<ModalLayer />
<MenuLayer />

View File

@@ -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
<TabContainerStyled tabVisible={tabVisible}>
<ErrorBoundary>
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
<TabComponent
{...props}
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
/>
</ErrorBoundary>
</TabContainerStyled>
);
@@ -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}
/>
);

View File

@@ -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 => (
<FileTabItem
{...tab}
title={tab.tooltip}
title={buildTooltip(tab)}
key={tab.tabid}
theme={theme}
onClick={e => handleTabClick(e, tab.tabid)}

View File

@@ -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 (
<AppObjectDiv
onContextMenu={handleContextMenu}
onClick={() => {
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}
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
{title}
{statusIcon && (
<StatusIconWrap>
<FontIcon icon={statusIcon} title={statusTitle} />
</StatusIconWrap>
)}
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
</AppObjectDiv>
<>
<AppObjectDiv
onContextMenu={handleContextMenu}
onClick={() => {
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}
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
{title}
{statusIcon && (
<StatusIconWrap>
<FontIcon icon={statusIcon} title={statusTitle} />
</StatusIconWrap>
)}
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
</AppObjectDiv>
{children}
</>
);
}

View File

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

View File

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

View File

@@ -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: {

View File

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

View File

@@ -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') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>

View File

@@ -312,7 +312,7 @@ export default function DataGridCore(props) {
}
}
: null,
[formViewAvailable, display]
[formViewAvailable, display, openNewTab]
);
if (!columns || columns.length == 0) return <LoadingInfo wrapper message="Waiting for structure" />;
@@ -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: {},

View File

@@ -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: {

View File

@@ -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 (
<>
<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">
Kill
</ToolbarButton>
{hasPermission('files/query/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>

View File

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

View File

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

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 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 =

View File

@@ -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(<App />, document.getElementById('root'));

View File

@@ -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') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton onClick={showPreview} icon="icon preview">
Preview
</ToolbarButton>

View File

@@ -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({
<ModalHeader modalState={modalState}>Import/Export {busy && <FontIcon icon="icon loading" />}</ModalHeader>
<Wrapper>
<ContentWrapper theme={theme}>
<ImportExportConfigurator uploadedFile={uploadedFile} onChangePreview={setPreviewReader} />
<ImportExportConfigurator
uploadedFile={uploadedFile}
openedFile={openedFile}
onChangePreview={setPreviewReader}
/>
</ContentWrapper>
<WidgetColumnWrapper theme={theme}>
<WidgetColumnBar>

View File

@@ -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 (
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Save file</ModalHeader>
@@ -23,6 +60,25 @@ export default function SaveFileModal({ data, folder, format, modalState, name,
</ModalContent>
<ModalFooter>
<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>
</FormProvider>
</ModalBase>

View File

@@ -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 (
<SaveFileModal
data={data}
folder={folder}
format={format}
modalState={modalState}
name={savedFile || 'newFile'}
onSave={onSave}
/>
<>
<SaveFileModal
data={data}
folder={folder}
format={format}
modalState={saveFileModalState}
name={savedFile || 'newFile'}
filePath={savedFilePath}
fileExtension={fileExtension}
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 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
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
Kill
</ToolbarButton>
{hasPermission('files/sql/write') && (
{/* {hasPermission('files/sql/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
)} */}
<ToolbarButton onClick={format} icon="icon format-code">
Format
</ToolbarButton>

View File

@@ -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 (
<>
<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">
Show wizard
</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 } = {}) =>
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',

View File

@@ -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}
/>
<SaveTabModal
modalState={saveFileModalState}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={modelState.value}
format="json"
folder="charts"
tabid={tabid}
fileExtension="chart"
/>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<ChartToolbar save={saveFileModalState.open} modelState={modelState} dispatchModel={dispatchModel} />,
toolbarPortalRef.current
)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<ChartToolbar modelState={modelState} dispatchModel={dispatchModel} />
</ToolbarPortal>
</>
);
}

View File

@@ -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()}
/>
<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 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(
<MarkdownToolbar save={saveFileModalState.open} showPreview={showPreview} />,
toolbarPortalRef.current
)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<MarkdownToolbar showPreview={showPreview} />
</ToolbarPortal>
<SaveTabModal
modalState={saveFileModalState}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={editorData}
format="text"
folder="markdown"
tabid={tabid}
fileExtension="md"
/>
</>
);

View File

@@ -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
)}
</ResultTabs>
</VerticalSplitter>
{toolbarPortalRef &&
toolbarPortalRef.current &&
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<QueryDesignToolbar
modelState={modelState}
dispatchModel={dispatchModel}
isDatabaseDefined={conid && database}
execute={handleExecute}
busy={busy}
// cancel={handleCancel}
// format={handleFormatCode}
isConnected={!!sessionId}
kill={handleKill}
/>
</ToolbarPortal>
{statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<QueryDesignToolbar
modelState={modelState}
dispatchModel={dispatchModel}
isDatabaseDefined={conid && database}
execute={handleExecute}
busy={busy}
// cancel={handleCancel}
// format={handleFormatCode}
save={saveFileModalState.open}
isConnected={!!sessionId}
kill={handleKill}
/>,
toolbarPortalRef.current
)}
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<SaveTabModal
modalState={saveFileModalState}
// modalState={saveFileModalState}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={modelState.value}
format="json"
folder="query"
tabid={tabid}
fileExtension="qdesign"
/>
</>
);

View File

@@ -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
</ResultTabs>
)}
</VerticalSplitter>
{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(<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
modalState={saveFileModalState}
toolbarPortalRef={toolbarPortalRef}
tabVisible={tabVisible}
data={editorData}
format="text"
folder="sql"
tabid={tabid}
fileExtension="sql"
/>
</>
);

View File

@@ -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
/>
<RunnerOutputPane runnerId={runnerId} executeNumber={executeNumber} />
</VerticalSplitter>
{toolbarPortalRef &&
toolbarPortalRef.current &&
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<ShellToolbar
execute={handleExecute}
busy={busy}
cancel={handleCancel}
edit={handleEdit}
editAvailable={configRegex.test(editorData || '')}
/>
</ToolbarPortal>
{statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<ShellToolbar
execute={handleExecute}
busy={busy}
cancel={handleCancel}
edit={handleEdit}
editAvailable={configRegex.test(editorData || '')}
save={saveFileModalState.open}
/>,
toolbarPortalRef.current
)}
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<SaveTabModal
modalState={saveFileModalState}
toolbarPortalRef={toolbarPortalRef}
tabVisible={tabVisible}
data={editorData}
format="text"
folder="shell"
tabid={tabid}
fileExtension="js"
/>
</>
);

View File

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

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

View File

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

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 { 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;

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 ...
// ... 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];
}

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();
return (
<WidgetColumnBar>
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs" height="20%">
<ClosedTabsList />
</WidgetColumnBarItem>
{hasPermission('files/favorites/read') && (
<WidgetColumnBarItem title="Favorites" name="favorites" height="15%">
<WidgetColumnBarItem title="Favorites" name="favorites" height="20%">
<FavoritesList />
</WidgetColumnBarItem>
)}
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs">
<ClosedTabsList />
</WidgetColumnBarItem>
</WidgetColumnBar>
);
}

View File

@@ -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 (
<Container theme={theme}>
{name && (
<Item>
<FontIcon icon="icon database" /> {name}
</Item>
)}
{(displayName || server) && (
<Item>
<FontIcon icon="icon server" /> {displayName || server}
</Item>
)}
<StatusbarContainer>
{name && (
<StatusBarItem>
<FontIcon icon="icon database" /> {name}
</StatusBarItem>
)}
{(displayName || server) && (
<StatusBarItem>
<FontIcon icon="icon server" /> {displayName || server}
</StatusBarItem>
)}
{user && (
<Item>
<FontIcon icon="icon account" /> {user}
</Item>
)}
{user && (
<StatusBarItem>
<FontIcon icon="icon account" /> {user}
</StatusBarItem>
)}
{connection && status && (
<Item>
{status.name == 'pending' && (
{connection && status && (
<StatusBarItem>
{status.name == 'pending' && (
<>
<FontIcon icon="icon loading" /> Loading
</>
)}
{status.name == 'ok' && (
<>
<InfoWrapper theme={theme}>
<FontIcon icon="icon ok" />
</InfoWrapper>{' '}
Connected
</>
)}
{status.name == 'error' && (
<>
<ErrorWrapper theme={theme}>
<FontIcon icon="icon error" />
</ErrorWrapper>{' '}
Error
</>
)}
</StatusBarItem>
)}
{!connection && (
<StatusBarItem>
<>
<FontIcon icon="icon loading" /> Loading
<FontIcon icon="icon disconnected" /> Not connected
</>
)}
{status.name == 'ok' && (
<>
<InfoWrapper theme={theme}>
<FontIcon icon="icon ok" />
</InfoWrapper>{' '}
Connected
</>
)}
{status.name == 'error' && (
<>
<ErrorWrapper theme={theme}>
<FontIcon icon="icon error" />
</ErrorWrapper>{' '}
Error
</>
)}
</Item>
)}
{!connection && (
<Item>
<>
<FontIcon icon="icon disconnected" /> Not connected
</>
</Item>
)}
</StatusBarItem>
)}
</StatusbarContainer>
<StatusbarContainer ref={statusbarPortalRef}></StatusbarContainer>
</Container>
);
}

View File

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