diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js
index fc91e5aa..d9c5f86c 100644
--- a/packages/web/src/datagrid/DataGridCore.js
+++ b/packages/web/src/datagrid/DataGridCore.js
@@ -596,7 +596,19 @@ export default function DataGridCore(props) {
}
function exportGrid() {
- showModal((modalState) => );
+ showModal((modalState) => (
+
+ ));
}
function setCellValue(chs, cell, value) {
diff --git a/packages/web/src/impexp/ImportExportConfigurator.js b/packages/web/src/impexp/ImportExportConfigurator.js
index ca53e4d3..cd6e8376 100644
--- a/packages/web/src/impexp/ImportExportConfigurator.js
+++ b/packages/web/src/impexp/ImportExportConfigurator.js
@@ -4,7 +4,14 @@ import { Formik, Form, useFormik, useFormikContext } from 'formik';
import styled from 'styled-components';
import Select from 'react-select';
import { FontIcon } from '../icons';
-import { FormButtonRow, FormSubmit, FormReactSelect, FormConnectionSelect, FormDatabaseSelect } from '../utility/forms';
+import {
+ FormButtonRow,
+ FormSubmit,
+ FormReactSelect,
+ FormConnectionSelect,
+ FormDatabaseSelect,
+ FormTablesSelect,
+} from '../utility/forms';
import { useConnectionList, useDatabaseList } from '../utility/metadataLoaders';
const Wrapper = styled.div`
@@ -62,6 +69,7 @@ function SourceTargetConfig({
storageTypeField,
connectionIdField,
databaseNameField,
+ tablesField,
}) {
const types = [
{ value: 'database', label: 'Database' },
@@ -80,6 +88,8 @@ function SourceTargetConfig({
+
+
>
)}
@@ -88,23 +98,21 @@ function SourceTargetConfig({
export default function ImportExportConfigurator() {
return (
-
-
-
+
+
+
+
);
}
diff --git a/packages/web/src/impexp/ScriptCreator.js b/packages/web/src/impexp/ScriptCreator.js
new file mode 100644
index 00000000..8067fe55
--- /dev/null
+++ b/packages/web/src/impexp/ScriptCreator.js
@@ -0,0 +1,49 @@
+import ScriptWriter from './ScriptWriter';
+
+export default class ScriptCreator {
+ constructor() {
+ this.varCount = 0;
+ this.commands = [];
+ }
+ allocVariable(prefix = 'var') {
+ this.varCount += 1;
+ return `${prefix}${this.varCount}`;
+ }
+ getCode() {
+ const writer = new ScriptWriter();
+ for (const command of this.commands) {
+ const { type } = command;
+ switch (type) {
+ case 'assign':
+ {
+ const { variableName, functionName, props } = command;
+ writer.assign(variableName, functionName, props);
+ }
+ break;
+ case 'copyStream':
+ {
+ const { sourceVar, targetVar } = command;
+ writer.copyStream(sourceVar, targetVar);
+ }
+ break;
+ }
+ }
+ writer.finish();
+ return writer.s;
+ }
+ assign(variableName, functionName, props) {
+ this.commands.push({
+ type: 'assign',
+ variableName,
+ functionName,
+ props,
+ });
+ }
+ copyStream(sourceVar, targetVar) {
+ this.commands.push({
+ type: 'copyStream',
+ sourceVar,
+ targetVar,
+ });
+ }
+}
diff --git a/packages/web/src/impexp/ScriptWriter.js b/packages/web/src/impexp/ScriptWriter.js
new file mode 100644
index 00000000..5da762f3
--- /dev/null
+++ b/packages/web/src/impexp/ScriptWriter.js
@@ -0,0 +1,26 @@
+export default class ScriptWriter {
+ constructor() {
+ this.s = '';
+ this.put('const dbgateApi = require("@dbgate/api");');
+ this.put('async function run() {');
+ }
+
+ put(s) {
+ this.s += s;
+ this.s += '\n';
+ }
+
+ finish() {
+ this.put('await dbgateApi.copyStream(queryReader, csvWriter);');
+ this.put('dbgateApi.runScript(run);');
+ this.put('}');
+ }
+
+ assign(variableName, functionName, props) {
+ this.put(`const ${variableName} = await dbgateApi.${functionName}(${JSON.stringify(props)});`);
+ }
+
+ copyStream(sourceVar, targetVar) {
+ this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
+ }
+}
diff --git a/packages/web/src/impexp/createImpExpScript.js b/packages/web/src/impexp/createImpExpScript.js
new file mode 100644
index 00000000..39cb61fa
--- /dev/null
+++ b/packages/web/src/impexp/createImpExpScript.js
@@ -0,0 +1,50 @@
+import ScriptCreator from './ScriptCreator';
+import getAsArray from '../utility/getAsArray';
+import { getConnectionInfo } from '../utility/metadataLoaders';
+import engines from '@dbgate/engines';
+
+function splitFullName(name) {
+ const i = name.indexOf('.');
+ if (i >= 0)
+ return {
+ schemaName: name.substr(0, i),
+ pureName: name.substr(i + 1),
+ };
+ return {
+ schemaName: null,
+ pureName: name,
+ };
+}
+function quoteFullName(dialect, { schemaName, pureName }) {
+ if (schemaName) return `${dialect.quoteIdentifier(schemaName)}.${dialect.quoteIdentifier(pureName)}`;
+ return `${dialect.quoteIdentifier(pureName)}`;
+}
+
+export default async function createImpExpScript(values) {
+ const script = new ScriptCreator();
+ if (values.sourceStorageType == 'database') {
+ const tables = getAsArray(values.sourceTables);
+ for (const table of tables) {
+ const sourceVar = script.allocVariable();
+ const connection = await getConnectionInfo({ conid: values.sourceConnectionId });
+ const driver = engines(connection);
+
+ const fullName = splitFullName(table);
+ script.assign(sourceVar, 'queryReader', {
+ connection: {
+ ...connection,
+ database: values.sourceDatabaseName,
+ },
+ sql: `select * from ${quoteFullName(driver.dialect, fullName)}`,
+ });
+
+ const targetVar = script.allocVariable();
+ script.assign(targetVar, 'csvWriter', {
+ fileName: `${fullName.pureName}.csv`,
+ });
+
+ script.copyStream(sourceVar, targetVar);
+ }
+ }
+ return script.getCode();
+}
diff --git a/packages/web/src/index.js b/packages/web/src/index.js
index b6607d8e..7adc4114 100644
--- a/packages/web/src/index.js
+++ b/packages/web/src/index.js
@@ -9,6 +9,7 @@ import 'ace-builds/src-noconflict/mode-sql';
import 'ace-builds/src-noconflict/mode-mysql';
import 'ace-builds/src-noconflict/mode-pgsql';
import 'ace-builds/src-noconflict/mode-sqlserver';
+import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-language_tools';
diff --git a/packages/web/src/modals/ImportExportModal.js b/packages/web/src/modals/ImportExportModal.js
index 6d2f49d5..042e1dc9 100644
--- a/packages/web/src/modals/ImportExportModal.js
+++ b/packages/web/src/modals/ImportExportModal.js
@@ -10,17 +10,43 @@ import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent';
import { useConnectionList, useDatabaseList } from '../utility/metadataLoaders';
import ImportExportConfigurator from '../impexp/ImportExportConfigurator';
+import createImpExpScript from '../impexp/createImpExpScript';
+import { openNewTab } from '../utility/common';
+import { useSetOpenedTabs } from '../utility/globalState';
+import { Formik, Form, useFormik, useFormikContext } from 'formik';
-export default function ImportExportModal({ modalState }) {
+export default function ImportExportModal({ modalState, initialValues }) {
+ const setOpenedTabs = useSetOpenedTabs();
+
+ const handleSubmit = async (values) => {
+ const code = await createImpExpScript(values);
+ openNewTab(setOpenedTabs, {
+ title: 'Shell',
+ icon: 'trigger.svg',
+ tabComponent: 'ShellTab',
+ props: {
+ initialScript: code,
+ },
+ });
+ modalState.close();
+ };
return (
- Import/Export
-
-
-
-
-
-
+
+
+
);
}
diff --git a/packages/web/src/sqleditor/JavaScriptEditor.js b/packages/web/src/sqleditor/JavaScriptEditor.js
new file mode 100644
index 00000000..40f03bca
--- /dev/null
+++ b/packages/web/src/sqleditor/JavaScriptEditor.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import styled from 'styled-components';
+import AceEditor from 'react-ace';
+import useDimensions from '../utility/useDimensions';
+
+const Wrapper = styled.div`
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+`;
+
+export default function JavaScriptEditor({
+ value = undefined,
+ readOnly = false,
+ onChange = undefined,
+ tabVisible = false,
+ onKeyDown = undefined,
+ editorRef = undefined,
+ focusOnCreate = false,
+}) {
+ const [containerRef, { height, width }] = useDimensions();
+ const ownEditorRef = React.useRef(null);
+
+ const currentEditorRef = editorRef || ownEditorRef;
+
+ React.useEffect(() => {
+ if ((tabVisible || focusOnCreate) && currentEditorRef.current && currentEditorRef.current.editor)
+ currentEditorRef.current.editor.focus();
+ }, [tabVisible, focusOnCreate]);
+
+ const handleKeyDown = React.useCallback(
+ async (data, hash, keyString, keyCode, event) => {
+ if (onKeyDown) onKeyDown(data, hash, keyString, keyCode, event);
+ },
+ [onKeyDown]
+ );
+
+ React.useEffect(() => {
+ if ((onKeyDown || !readOnly) && currentEditorRef.current) {
+ currentEditorRef.current.editor.keyBinding.addKeyboardHandler(handleKeyDown);
+ }
+ return () => {
+ currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(handleKeyDown);
+ };
+ }, [handleKeyDown]);
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/web/src/tabs/ShellTab.js b/packages/web/src/tabs/ShellTab.js
new file mode 100644
index 00000000..b3652dc5
--- /dev/null
+++ b/packages/web/src/tabs/ShellTab.js
@@ -0,0 +1,108 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import _ from 'lodash';
+import axios from '../utility/axios';
+import engines from '@dbgate/engines';
+
+import { useConnectionInfo, getTableInfo, getConnectionInfo, getSqlObjectInfo } from '../utility/metadataLoaders';
+import SqlEditor from '../sqleditor/SqlEditor';
+import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState';
+import QueryToolbar from '../query/QueryToolbar';
+import SessionMessagesView from '../query/SessionMessagesView';
+import { TabPage } from '../widgets/TabControl';
+import ResultTabs from '../sqleditor/ResultTabs';
+import { VerticalSplitter } from '../widgets/Splitter';
+import keycodes from '../utility/keycodes';
+import { changeTab } from '../utility/common';
+import useSocket from '../utility/SocketProvider';
+import SaveSqlFileModal from '../modals/SaveSqlFileModal';
+import useModalState from '../modals/useModalState';
+import sqlFormatter from 'sql-formatter';
+import JavaScriptEditor from '../sqleditor/JavaScriptEditor';
+
+export default function ShellTab({
+ tabid,
+ conid,
+ database,
+ initialArgs,
+ tabVisible,
+ toolbarPortalRef,
+ initialScript,
+ storageKey,
+ ...other
+}) {
+ const localStorageKey = storageKey || `shell_${tabid}`;
+ const [shellText, setShellText] = React.useState(() => localStorage.getItem(localStorageKey) || initialScript || '');
+ const shellTextRef = React.useRef(shellText);
+ const [busy, setBusy] = React.useState(false);
+
+ const saveToStorage = React.useCallback(() => localStorage.setItem(localStorageKey, shellTextRef.current), [
+ localStorageKey,
+ shellTextRef,
+ ]);
+ const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 5000), [saveToStorage]);
+ const setOpenedTabs = useSetOpenedTabs();
+
+ React.useEffect(() => {
+ window.addEventListener('beforeunload', saveToStorage);
+ return () => {
+ saveToStorage();
+ window.removeEventListener('beforeunload', saveToStorage);
+ };
+ }, []);
+
+ React.useEffect(() => {
+ if (!storageKey)
+ changeTab(tabid, setOpenedTabs, (tab) => ({
+ ...tab,
+ props: {
+ ...tab.props,
+ storageKey: localStorageKey,
+ },
+ }));
+ }, [storageKey]);
+
+ React.useEffect(() => {
+ changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, busy }));
+ }, [busy]);
+
+ const editorRef = React.useRef(null);
+
+ useUpdateDatabaseForTab(tabVisible, conid, database);
+ const connection = useConnectionInfo({ conid });
+
+ const handleChange = (text) => {
+ if (text != null) shellTextRef.current = text;
+ setShellText(text);
+ saveToStorageDebounced();
+ };
+
+ const handleExecute = async () => {};
+
+ const handleCancel = () => {
+ // axios.post('sessions/cancel', {
+ // sesid: sessionId,
+ // });
+ };
+
+ const handleKeyDown = (data, hash, keyString, keyCode, event) => {
+ if (keyCode == keycodes.f5) {
+ event.preventDefault();
+ handleExecute();
+ }
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index 6a674165..5b3d72ab 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -2,6 +2,7 @@ import TableDataTab from './TableDataTab';
import ViewDataTab from './ViewDataTab';
import TableStructureTab from './TableStructureTab';
import QueryTab from './QueryTab';
+import ShellTab from './ShellTab';
import InfoPageTab from './InfoPageTab';
export default {
@@ -10,4 +11,5 @@ export default {
TableStructureTab,
QueryTab,
InfoPageTab,
+ ShellTab,
};
diff --git a/packages/web/src/utility/forms.js b/packages/web/src/utility/forms.js
index ba3f3e74..e9ad0300 100644
--- a/packages/web/src/utility/forms.js
+++ b/packages/web/src/utility/forms.js
@@ -4,8 +4,9 @@ import Select from 'react-select';
import { TextField, SelectField } from './inputs';
import { Field, useFormikContext } from 'formik';
import FormStyledButton from '../widgets/FormStyledButton';
-import { useConnectionList, useDatabaseList } from './metadataLoaders';
+import { useConnectionList, useDatabaseList, useDatabaseInfo } from './metadataLoaders';
import useSocket from './SocketProvider';
+import getAsArray from './getAsArray';
export const FormRow = styled.div`
display: flex;
@@ -84,14 +85,23 @@ export function FormRadioGroupItem({ name, text, value }) {
);
}
-export function FormReactSelect({ options, name }) {
+export function FormReactSelect({ options, name, isMulti = false }) {
const { setFieldValue, values } = useFormikContext();
+
return (