mirror of
https://github.com/dbgate/dbgate
synced 2024-11-22 16:27:18 +00:00
generate shell script
This commit is contained in:
parent
f03a8c258a
commit
74aa90fd2a
@ -596,7 +596,19 @@ export default function DataGridCore(props) {
|
||||
}
|
||||
|
||||
function exportGrid() {
|
||||
showModal((modalState) => <ImportExportModal modalState={modalState} />);
|
||||
showModal((modalState) => (
|
||||
<ImportExportModal
|
||||
modalState={modalState}
|
||||
initialValues={{
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: conid,
|
||||
sourceDatabaseName: database,
|
||||
sourceTables: [
|
||||
`${display.baseTable && display.baseTable.schemaName}.${display.baseTable && display.baseTable.pureName}`,
|
||||
],
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function setCellValue(chs, cell, value) {
|
||||
|
@ -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({
|
||||
<FormConnectionSelect name={connectionIdField} />
|
||||
<Label>Database</Label>
|
||||
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} />
|
||||
<Label>Tables/views</Label>
|
||||
<FormTablesSelect conidName={connectionIdField} databaseName={databaseNameField} name={tablesField} />
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
@ -88,23 +98,21 @@ function SourceTargetConfig({
|
||||
|
||||
export default function ImportExportConfigurator() {
|
||||
return (
|
||||
<Formik onSubmit={null} initialValues={{ sourceStorageType: 'database', targetStorageType: 'csv' }}>
|
||||
<Form>
|
||||
<Wrapper>
|
||||
<SourceTargetConfig
|
||||
isSource
|
||||
storageTypeField="sourceStorageType"
|
||||
connectionIdField="sourceConnectionId"
|
||||
databaseNameField="sourceDatabaseName"
|
||||
/>
|
||||
<SourceTargetConfig
|
||||
isTarget
|
||||
storageTypeField="targetStorageType"
|
||||
connectionIdField="targetConnectionId"
|
||||
databaseNameField="targetDatabaseName"
|
||||
/>
|
||||
</Wrapper>
|
||||
</Form>
|
||||
</Formik>
|
||||
<Wrapper>
|
||||
<SourceTargetConfig
|
||||
isSource
|
||||
storageTypeField="sourceStorageType"
|
||||
connectionIdField="sourceConnectionId"
|
||||
databaseNameField="sourceDatabaseName"
|
||||
tablesField="sourceTables"
|
||||
/>
|
||||
<SourceTargetConfig
|
||||
isTarget
|
||||
storageTypeField="targetStorageType"
|
||||
connectionIdField="targetConnectionId"
|
||||
databaseNameField="targetDatabaseName"
|
||||
tablesField="targetTables"
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
49
packages/web/src/impexp/ScriptCreator.js
Normal file
49
packages/web/src/impexp/ScriptCreator.js
Normal file
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
26
packages/web/src/impexp/ScriptWriter.js
Normal file
26
packages/web/src/impexp/ScriptWriter.js
Normal file
@ -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});`);
|
||||
}
|
||||
}
|
50
packages/web/src/impexp/createImpExpScript.js
Normal file
50
packages/web/src/impexp/createImpExpScript.js
Normal file
@ -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();
|
||||
}
|
@ -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';
|
||||
|
@ -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 (
|
||||
<ModalBase modalState={modalState}>
|
||||
<ModalHeader modalState={modalState}>Import/Export</ModalHeader>
|
||||
<ModalContent>
|
||||
<ImportExportConfigurator />
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
|
||||
</ModalFooter>
|
||||
<Formik
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{ sourceStorageType: 'database', targetStorageType: 'csv', ...initialValues }}
|
||||
>
|
||||
<Form>
|
||||
<ModalHeader modalState={modalState}>Import/Export</ModalHeader>
|
||||
<ModalContent>
|
||||
<ImportExportConfigurator />
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<FormStyledButton type="submit" value="Export" />
|
||||
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</Formik>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
|
69
packages/web/src/sqleditor/JavaScriptEditor.js
Normal file
69
packages/web/src/sqleditor/JavaScriptEditor.js
Normal file
@ -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 (
|
||||
<Wrapper ref={containerRef}>
|
||||
<AceEditor
|
||||
ref={currentEditorRef}
|
||||
mode="javascript"
|
||||
theme="github"
|
||||
onChange={onChange}
|
||||
name="UNIQUE_ID_OF_DIV"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{
|
||||
showPrintMargin: false,
|
||||
}}
|
||||
value={value}
|
||||
readOnly={readOnly}
|
||||
fontSize="11pt"
|
||||
width={`${width}px`}
|
||||
height={`${height}px`}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
108
packages/web/src/tabs/ShellTab.js
Normal file
108
packages/web/src/tabs/ShellTab.js
Normal file
@ -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 (
|
||||
<>
|
||||
<VerticalSplitter>
|
||||
<JavaScriptEditor
|
||||
value={shellText}
|
||||
onChange={handleChange}
|
||||
tabVisible={tabVisible}
|
||||
onKeyDown={handleKeyDown}
|
||||
editorRef={editorRef}
|
||||
/>
|
||||
</VerticalSplitter>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -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 (
|
||||
<Select
|
||||
options={options}
|
||||
defaultValue={options.find((x) => x.value == values[name])}
|
||||
onChange={(item) => setFieldValue(name, item ? item.value : null)}
|
||||
defaultValue={
|
||||
isMulti
|
||||
? options.filter((x) => values[name] && values[name].includes(x.value))
|
||||
: options.find((x) => x.value == values[name])
|
||||
}
|
||||
onChange={(item) =>
|
||||
setFieldValue(name, isMulti ? getAsArray(item).map((x) => x.value) : item ? item.value : null)
|
||||
}
|
||||
menuPortalTarget={document.body}
|
||||
isMulti={isMulti}
|
||||
closeMenuOnSelect={!isMulti}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -126,3 +136,19 @@ export function FormDatabaseSelect({ conidName, name }) {
|
||||
if (databaseOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={databaseOptions} name={name} />;
|
||||
}
|
||||
|
||||
export function FormTablesSelect({ conidName, databaseName, name }) {
|
||||
const { values } = useFormikContext();
|
||||
const dbinfo = useDatabaseInfo({ conid: values[conidName], database: values[databaseName] });
|
||||
const tablesOptions = React.useMemo(
|
||||
() =>
|
||||
[...((dbinfo && dbinfo.tables) || []), ...((dbinfo && dbinfo.views) || [])].map((x) => ({
|
||||
value: `${x.schemaName}.${x.pureName}`,
|
||||
label: x.pureName,
|
||||
})),
|
||||
[dbinfo]
|
||||
);
|
||||
|
||||
if (tablesOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={tablesOptions} name={name} isMulti />;
|
||||
}
|
||||
|
@ -23,3 +23,24 @@ async function run() {
|
||||
}
|
||||
|
||||
dbgateApi.runScript(run);
|
||||
|
||||
// dbgateApi.runBatch([
|
||||
// {
|
||||
// type: 'copyStream',
|
||||
// source: {
|
||||
// type: 'queryReader',
|
||||
// connection: {
|
||||
// server: 'localhost',
|
||||
// engine: 'mysql',
|
||||
// user: 'root',
|
||||
// password: 'test',
|
||||
// port: '3307',
|
||||
// database: 'Chinook',
|
||||
// },
|
||||
// sql: 'SELECT * FROM Genre',
|
||||
// },
|
||||
// target: {
|
||||
// type: 'csvWriter',
|
||||
// },
|
||||
// },
|
||||
// ]);
|
||||
|
Loading…
Reference in New Issue
Block a user