generate shell script

This commit is contained in:
Jan Prochazka 2020-06-06 21:48:29 +02:00
parent f03a8c258a
commit 74aa90fd2a
12 changed files with 430 additions and 32 deletions

View File

@ -596,7 +596,19 @@ export default function DataGridCore(props) {
} }
function exportGrid() { 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) { function setCellValue(chs, cell, value) {

View File

@ -4,7 +4,14 @@ import { Formik, Form, useFormik, useFormikContext } from 'formik';
import styled from 'styled-components'; import styled from 'styled-components';
import Select from 'react-select'; import Select from 'react-select';
import { FontIcon } from '../icons'; 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'; import { useConnectionList, useDatabaseList } from '../utility/metadataLoaders';
const Wrapper = styled.div` const Wrapper = styled.div`
@ -62,6 +69,7 @@ function SourceTargetConfig({
storageTypeField, storageTypeField,
connectionIdField, connectionIdField,
databaseNameField, databaseNameField,
tablesField,
}) { }) {
const types = [ const types = [
{ value: 'database', label: 'Database' }, { value: 'database', label: 'Database' },
@ -80,6 +88,8 @@ function SourceTargetConfig({
<FormConnectionSelect name={connectionIdField} /> <FormConnectionSelect name={connectionIdField} />
<Label>Database</Label> <Label>Database</Label>
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} /> <FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} />
<Label>Tables/views</Label>
<FormTablesSelect conidName={connectionIdField} databaseName={databaseNameField} name={tablesField} />
</> </>
)} )}
</Column> </Column>
@ -88,23 +98,21 @@ function SourceTargetConfig({
export default function ImportExportConfigurator() { export default function ImportExportConfigurator() {
return ( return (
<Formik onSubmit={null} initialValues={{ sourceStorageType: 'database', targetStorageType: 'csv' }}> <Wrapper>
<Form> <SourceTargetConfig
<Wrapper> isSource
<SourceTargetConfig storageTypeField="sourceStorageType"
isSource connectionIdField="sourceConnectionId"
storageTypeField="sourceStorageType" databaseNameField="sourceDatabaseName"
connectionIdField="sourceConnectionId" tablesField="sourceTables"
databaseNameField="sourceDatabaseName" />
/> <SourceTargetConfig
<SourceTargetConfig isTarget
isTarget storageTypeField="targetStorageType"
storageTypeField="targetStorageType" connectionIdField="targetConnectionId"
connectionIdField="targetConnectionId" databaseNameField="targetDatabaseName"
databaseNameField="targetDatabaseName" tablesField="targetTables"
/> />
</Wrapper> </Wrapper>
</Form>
</Formik>
); );
} }

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

View 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});`);
}
}

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

View File

@ -9,6 +9,7 @@ import 'ace-builds/src-noconflict/mode-sql';
import 'ace-builds/src-noconflict/mode-mysql'; import 'ace-builds/src-noconflict/mode-mysql';
import 'ace-builds/src-noconflict/mode-pgsql'; import 'ace-builds/src-noconflict/mode-pgsql';
import 'ace-builds/src-noconflict/mode-sqlserver'; 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/theme-github';
import 'ace-builds/src-noconflict/ext-searchbox'; import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-language_tools'; import 'ace-builds/src-noconflict/ext-language_tools';

View File

@ -10,17 +10,43 @@ import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent'; import ModalContent from './ModalContent';
import { useConnectionList, useDatabaseList } from '../utility/metadataLoaders'; import { useConnectionList, useDatabaseList } from '../utility/metadataLoaders';
import ImportExportConfigurator from '../impexp/ImportExportConfigurator'; 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 ( return (
<ModalBase modalState={modalState}> <ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>Import/Export</ModalHeader> <Formik
<ModalContent> onSubmit={handleSubmit}
<ImportExportConfigurator /> initialValues={{ sourceStorageType: 'database', targetStorageType: 'csv', ...initialValues }}
</ModalContent> >
<ModalFooter> <Form>
<FormStyledButton type="button" value="Close" onClick={modalState.close} /> <ModalHeader modalState={modalState}>Import/Export</ModalHeader>
</ModalFooter> <ModalContent>
<ImportExportConfigurator />
</ModalContent>
<ModalFooter>
<FormStyledButton type="submit" value="Export" />
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
</ModalFooter>
</Form>
</Formik>
</ModalBase> </ModalBase>
); );
} }

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

View 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>
</>
);
}

View File

@ -2,6 +2,7 @@ import TableDataTab from './TableDataTab';
import ViewDataTab from './ViewDataTab'; import ViewDataTab from './ViewDataTab';
import TableStructureTab from './TableStructureTab'; import TableStructureTab from './TableStructureTab';
import QueryTab from './QueryTab'; import QueryTab from './QueryTab';
import ShellTab from './ShellTab';
import InfoPageTab from './InfoPageTab'; import InfoPageTab from './InfoPageTab';
export default { export default {
@ -10,4 +11,5 @@ export default {
TableStructureTab, TableStructureTab,
QueryTab, QueryTab,
InfoPageTab, InfoPageTab,
ShellTab,
}; };

View File

@ -4,8 +4,9 @@ import Select from 'react-select';
import { TextField, SelectField } from './inputs'; import { TextField, SelectField } from './inputs';
import { Field, useFormikContext } from 'formik'; import { Field, useFormikContext } from 'formik';
import FormStyledButton from '../widgets/FormStyledButton'; import FormStyledButton from '../widgets/FormStyledButton';
import { useConnectionList, useDatabaseList } from './metadataLoaders'; import { useConnectionList, useDatabaseList, useDatabaseInfo } from './metadataLoaders';
import useSocket from './SocketProvider'; import useSocket from './SocketProvider';
import getAsArray from './getAsArray';
export const FormRow = styled.div` export const FormRow = styled.div`
display: flex; 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(); const { setFieldValue, values } = useFormikContext();
return ( return (
<Select <Select
options={options} options={options}
defaultValue={options.find((x) => x.value == values[name])} defaultValue={
onChange={(item) => setFieldValue(name, item ? item.value : null)} 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} 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>; if (databaseOptions.length == 0) return <div>Not available</div>;
return <FormReactSelect options={databaseOptions} name={name} />; 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 />;
}

View File

@ -23,3 +23,24 @@ async function run() {
} }
dbgateApi.runScript(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',
// },
// },
// ]);