import export

This commit is contained in:
Jan Prochazka 2021-03-11 20:37:05 +01:00
parent c2c54856ff
commit 2063005d5c
12 changed files with 716 additions and 13 deletions

View File

@ -109,6 +109,15 @@
onClick: () => get(currentDataGrid).copyToClipboard(),
});
registerCommand({
id: 'dataGrid.export',
category: 'Data grid',
name: 'Export',
keyText: 'Ctrl+E',
enabledStore: derived(currentDataGrid, grid => grid != null && grid.exportEnabled()),
onClick: () => get(currentDataGrid).exportGrid(),
});
function getRowCountInfo(selectedCells, grider, realColumnUniqueNames, selectedRowData, allRowCount) {
if (selectedCells.length > 1 && selectedCells.every(x => _.isNumber(x[0]) && _.isNumber(x[1]))) {
let sum = _.sumBy(selectedCells, cell => {
@ -180,6 +189,7 @@
export let onReferenceClick = undefined;
export let onSave;
export let focusOnVisible = false;
export let onExportGrid = null;
export let isLoadedAll;
export let loadedTime;
@ -231,6 +241,14 @@
return display;
}
export function exportGrid() {
if (onExportGrid) onExportGrid();
}
export function exportEnabled() {
return !!onExportGrid;
}
export function revertRowChanges() {
grider.beginUpdate();
for (const index of getSelectedRowIndexes()) {
@ -813,6 +831,7 @@
return [
{ command: 'dataGrid.refresh' },
{ command: 'dataGrid.copyToClipboard' },
{ command: 'dataGrid.export' },
{ divider: true },
{ command: 'dataGrid.save' },
{ command: 'dataGrid.revertRowChanges' },

View File

@ -48,6 +48,7 @@
import { scriptToSql } from 'dbgate-sqltree';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import axiosInstance from '../utility/axiosInstance';
@ -98,6 +99,16 @@
engine: display.engine,
});
}
function exportGrid() {
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceSql = display.getExportQuery();
initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : [];
showModal(ImportExportModal, { initialValues });
}
</script>
<LoadingDataGridCore
@ -105,6 +116,7 @@
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
onExportGrid={exportGrid}
bind:loadedRows
{grider}
onSave={handleSave}

View File

@ -0,0 +1,54 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
import { createEventDispatcher } from 'svelte';
export let icon;
export let disabled = false;
const dispatch = createEventDispatcher();
function handleClick() {
if (!disabled) dispatch('click');
}
</script>
<div class="button" on:click={handleClick} class:disabled>
<div class="icon">
<FontIcon {icon} />
</div>
<div class="inner">
<slot />
</div>
</div>
<style>
.button {
padding: 5px 15px;
color: var(--theme-font-1);
border: 1px solid var(--theme-border);
width: 120px;
height: 60px;
background-color: var(--theme-bg-1);
}
.button:not(.disabled):hover {
background-color: var(--theme-bg-2);
}
.button:not(.disabled):active {
background-color: var(--theme-bg-3);
}
.button.disabled {
color: var(--theme-font-3);
}
.icon {
font-size: 30px;
text-align: center;
}
.inner {
text-align: center;
}
</style>

View File

@ -0,0 +1,17 @@
<script>
import { getFormContext } from './FormProviderCore.svelte';
import { createEventDispatcher } from 'svelte';
import LargeButton from '../elements/LargeButton.svelte';
const dispatch = createEventDispatcher();
const { values } = getFormContext();
function handleClick() {
dispatch('click', $values);
}
</script>
<LargeButton on:click={handleClick} {...$$props}>
<slot />
</LargeButton>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import FormSelectField from '../forms/FormSelectField.svelte';
import { useConnectionList } from '../utility/metadataLoaders';
$: connections = useConnectionList();
$: connectionOptions = ($connections || []).map(conn => ({
value: conn._id,
label: conn.displayName || conn.server,
}));
</script>
{#if connectionOptions.length == 0}
<div>Not available</div>
{:else}
<FormSelectField {...$$restProps} options={connectionOptions} />
{/if}

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { getFormContext } from '../forms/FormProviderCore.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import { useDatabaseList } from '../utility/metadataLoaders';
export let conidName;
const { values } = getFormContext();
$: databases = useDatabaseList({ conid: $values[conidName] });
$: databaseOptions = ($databases || []).map(db => ({
value: db.name,
label: db.name,
}));
</script>
{#if databaseOptions.length == 0}
<div>Not available</div>
{:else}
<FormSelectField {...$$restProps} options={databaseOptions} />
{/if}

View File

@ -0,0 +1,41 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
import SourceTargetConfig from './SourceTargetConfig.svelte';
// engine={sourceEngine}
// {setPreviewSource}
</script>
<div class="flex1">
<div class="flex">
<SourceTargetConfig
direction="source"
storageTypeField="sourceStorageType"
connectionIdField="sourceConnectionId"
databaseNameField="sourceDatabaseName"
archiveFolderField="sourceArchiveFolder"
schemaNameField="sourceSchemaName"
tablesField="sourceList"
/>
<div class="arrow">
<FontIcon icon="icon arrow-right" />
</div>
<SourceTargetConfig
direction="target"
storageTypeField="targetStorageType"
connectionIdField="targetConnectionId"
databaseNameField="targetDatabaseName"
archiveFolderField="targetArchiveFolder"
schemaNameField="targetSchemaName"
/>
</div>
</div>
<style>
.arrow {
font-size: 30px;
color: var(--theme-icon-blue);
align-self: center;
}
</style>

View File

@ -0,0 +1,50 @@
import _ from 'lodash';
import { extractShellApiFunctionName, extractShellApiPlugins } from 'dbgate-tools';
export default class ScriptWriter {
s = '';
packageNames: string[] = [];
varCount = 0;
constructor(varCount = '0') {
this.varCount = parseInt(varCount) || 0;
}
allocVariable(prefix = 'var') {
this.varCount += 1;
return `${prefix}${this.varCount}`;
}
put(s = '') {
this.s += s;
this.s += '\n';
}
assign(variableName, functionName, props) {
this.put(`const ${variableName} = await ${extractShellApiFunctionName(functionName)}(${JSON.stringify(props)});`);
this.packageNames.push(...extractShellApiPlugins(functionName, props));
}
requirePackage(packageName) {
this.packageNames.push(packageName);
}
copyStream(sourceVar, targetVar) {
this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
}
comment(s) {
this.put(`// ${s}`);
}
getScript(schedule = null) {
const packageNames = this.packageNames;
let prefix = _.uniq(packageNames)
.map(packageName => `// @require ${packageName}\n`)
.join('');
if (schedule) prefix += `// @schedule ${schedule}`;
if (prefix) prefix += '\n';
return prefix + this.s;
}
}

View File

@ -0,0 +1,79 @@
<script lang="ts">
import { getFormContext } from '../forms/FormProviderCore.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { findFileFormat, getFileFormatDirections } from '../plugins/fileformats';
import { extensions } from '../stores';
import { useArchiveFiles, useDatabaseInfo } from '../utility/metadataLoaders';
import FormConnectionSelect from './FormConnectionSelect.svelte';
import FormDatabaseSelect from './FormDatabaseSelect.svelte';
export let direction;
export let storageTypeField;
export let connectionIdField;
export let databaseNameField;
export let archiveFolderField;
export let schemaNameField;
export let tablesField = undefined;
export let engine = undefined;
const { values, setFieldValue } = getFormContext();
$: types =
$values[storageTypeField] == 'jsldata'
? [{ value: 'jsldata', label: 'Query result data', directions: ['source'] }]
: [
{ value: 'database', label: 'Database', directions: ['source', 'target'] },
...$extensions.fileFormats.map(format => ({
value: format.storageType,
label: `${format.name} files(s)`,
directions: getFileFormatDirections(format),
})),
{ value: 'query', label: 'SQL Query', directions: ['source'] },
{ value: 'archive', label: 'Archive', directions: ['source', 'target'] },
];
$: storageType = $values[storageTypeField];
$: dbinfo = useDatabaseInfo({ conid: $values[connectionIdField], database: $values[databaseNameField] });
$: archiveFiles = useArchiveFiles({ folder: $values[archiveFolderField] });
$: format = findFileFormat($extensions, storageType);
</script>
<div class="column">
{#if direction == 'source'}
<div class="title">
<FontIcon icon="icon import" /> Source configuration
</div>
{/if}
{#if direction == 'target'}
<div class="title">
<FontIcon icon="icon export" /> Target configuration
</div>
{/if}
<FormSelectField
options={types.filter(x => x.directions.includes(direction))}
name={storageTypeField}
label="Storage type"
/>
{#if storageType == 'database' || storageType == 'query'}
<FormConnectionSelect name={connectionIdField} label="Server" />
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
{/if}
</div>
<style>
.title {
font-size: 20px;
text-align: center;
margin: 10px 0px;
}
.column {
margin: 10px;
flex: 1;
}
</style>

View File

@ -0,0 +1,240 @@
import _ from 'lodash';
import ScriptWriter from './ScriptWriter';
import getAsArray from '../utility/getAsArray';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { findEngineDriver, findObjectLike } from 'dbgate-tools';
import { findFileFormat } from '../plugins/fileformats';
export function getTargetName(extensions, source, values) {
const key = `targetName_${source}`;
if (values[key]) return values[key];
const format = findFileFormat(extensions, values.targetStorageType);
if (format) {
const res = format.getDefaultOutputName ? format.getDefaultOutputName(source, values) : null;
if (res) return res;
return `${source}.${format.extension}`;
}
return source;
}
function extractApiParameters(values, direction, format) {
const pairs = (format.args || [])
.filter(arg => arg.apiName)
.map(arg => [arg.apiName, values[`${direction}_${format.storageType}_${arg.name}`]])
.filter(x => x[1] != null);
return _.fromPairs(pairs);
}
async function getConnection(extensions, storageType, conid, database) {
if (storageType == 'database' || storageType == 'query') {
const conn = await getConnectionInfo({ conid });
const driver = findEngineDriver(conn, extensions);
return [
{
..._.omit(conn, ['_id', 'displayName']),
database,
},
driver,
];
}
return [null, null];
}
function getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver) {
const { sourceStorageType } = values;
if (sourceStorageType == 'database') {
const fullName = { schemaName: values.sourceSchemaName, pureName: sourceName };
return [
'tableReader',
{
connection: sourceConnection,
...fullName,
},
];
}
if (sourceStorageType == 'query') {
return [
'queryReader',
{
connection: sourceConnection,
sql: values.sourceSql,
},
];
}
if (findFileFormat(extensions, sourceStorageType)) {
const sourceFile = values[`sourceFile_${sourceName}`];
const format = findFileFormat(extensions, sourceStorageType);
if (format && format.readerFunc) {
return [
format.readerFunc,
{
..._.omit(sourceFile, ['isDownload']),
...extractApiParameters(values, 'source', format),
},
];
}
}
if (sourceStorageType == 'jsldata') {
return ['jslDataReader', { jslid: values.sourceJslId }];
}
if (sourceStorageType == 'archive') {
return [
'archiveReader',
{
folderName: values.sourceArchiveFolder,
fileName: sourceName,
},
];
}
throw new Error(`Unknown source storage type: ${sourceStorageType}`);
}
function getFlagsFroAction(action) {
switch (action) {
case 'dropCreateTable':
return {
createIfNotExists: true,
dropIfExists: true,
};
case 'truncate':
return {
createIfNotExists: true,
truncate: true,
};
}
return {
createIfNotExists: true,
};
}
function getTargetExpr(extensions, sourceName, values, targetConnection, targetDriver) {
const { targetStorageType } = values;
const format = findFileFormat(extensions, targetStorageType);
if (format && format.writerFunc) {
const outputParams = format.getOutputParams && format.getOutputParams(sourceName, values);
return [
format.writerFunc,
{
...(outputParams
? outputParams
: {
fileName: getTargetName(extensions, sourceName, values),
}),
...extractApiParameters(values, 'target', format),
},
];
}
if (targetStorageType == 'database') {
return [
'tableWriter',
{
connection: targetConnection,
schemaName: values.targetSchemaName,
pureName: getTargetName(extensions, sourceName, values),
...getFlagsFroAction(values[`actionType_${sourceName}`]),
},
];
}
if (targetStorageType == 'archive') {
return [
'archiveWriter',
{
folderName: values.targetArchiveFolder,
fileName: getTargetName(extensions, sourceName, values),
},
];
}
throw new Error(`Unknown target storage type: ${targetStorageType}`);
}
export default async function createImpExpScript(extensions, values, addEditorInfo = true) {
const script = new ScriptWriter(values.startVariableIndex || 0);
const [sourceConnection, sourceDriver] = await getConnection(
extensions,
values.sourceStorageType,
values.sourceConnectionId,
values.sourceDatabaseName
);
const [targetConnection, targetDriver] = await getConnection(
extensions,
values.targetStorageType,
values.targetConnectionId,
values.targetDatabaseName
);
const sourceList = getAsArray(values.sourceList);
for (const sourceName of sourceList) {
const sourceVar = script.allocVariable();
// @ts-ignore
script.assign(sourceVar, ...getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver));
const targetVar = script.allocVariable();
// @ts-ignore
script.assign(targetVar, ...getTargetExpr(extensions, sourceName, values, targetConnection, targetDriver));
script.copyStream(sourceVar, targetVar);
script.put();
}
if (addEditorInfo) {
script.comment('@ImportExportConfigurator');
script.comment(JSON.stringify(values));
}
return script.getScript(values.schedule);
}
export function getActionOptions(extensions, source, values, targetDbinfo) {
const res = [];
const targetName = getTargetName(extensions, source, values);
if (values.targetStorageType == 'database') {
let existing = findObjectLike(
{ schemaName: values.targetSchemaName, pureName: targetName },
targetDbinfo,
'tables'
);
if (existing) {
res.push({
label: 'Append data',
value: 'appendData',
});
res.push({
label: 'Truncate and import',
value: 'truncate',
});
res.push({
label: 'Drop and create table',
value: 'dropCreateTable',
});
} else {
res.push({
label: 'Create table',
value: 'createTable',
});
}
} else {
res.push({
label: 'Create file',
value: 'createFile',
});
}
return res;
}
export async function createPreviewReader(extensions, values, sourceName) {
const [sourceConnection, sourceDriver] = await getConnection(
extensions,
values.sourceStorageType,
values.sourceConnectionId,
values.sourceDatabaseName
);
const [functionName, props] = getSourceExpr(extensions, sourceName, values, sourceConnection, sourceDriver);
return {
functionName,
props: {
...props,
limitRows: 100,
},
};
}

View File

@ -0,0 +1,135 @@
<script lang="ts">
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import LargeButton from '../elements/LargeButton.svelte';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import LargeFormButton from '../forms/LargeFormButton.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import ImportExportConfigurator from '../impexp/ImportExportConfigurator.svelte';
import RunnerOutputFiles from '../query/RunnerOutputFiles';
import SocketMessageView from '../query/SocketMessageView.svelte';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
let busy = false;
let executeNumber = 0;
let runnerId = null;
let previewReader = null;
export let initialValues;
export let uploadedFile = undefined;
export let openedFile = undefined;
export let importToArchive = false;
const handleGenerateScript = async () => {
// const code = await createImpExpScript(extensions, values);
// openNewTab(
// {
// title: 'Shell #',
// icon: 'img shell',
// tabComponent: 'ShellTab',
// },
// { editor: code }
// );
// modalState.close();
};
const handleExecute = async values => {
// if (busy) return;
// setBusy(true);
// const script = await createImpExpScript(extensions, values);
// setExecuteNumber(num => num + 1);
// let runid = runnerId;
// const resp = await axios.post('runners/start', { script });
// runid = resp.data.runid;
// setRunnerId(runid);
// if (values.targetStorageType == 'archive') {
// refreshArchiveFolderRef.current = values.targetArchiveFolder;
// } else {
// refreshArchiveFolderRef.current = null;
// }
};
const handleCancel = () => {
// axios.post('runners/cancel', {
// runid: runnerId,
// });
};
</script>
<FormProvider>
<ModalBase {...$$restProps} fullScreen skipBody skipFooter>
<svelte:fragment slot="header">
Import/Export
{#if busy}
<FontIcon icon="icon loading" />
{/if}
</svelte:fragment>
<div class="wrapper">
<HorizontalSplitter>
<svelte:fragment slot="1">
<ImportExportConfigurator />
</svelte:fragment>
<svelte:fragment slot="2">
<WidgetColumnBar>
<WidgetColumnBarItem title="Output files" name="output" height="20%">
<RunnerOutputFiles {runnerId} {executeNumber} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Messages" name="messages">
<SocketMessageView eventName={runnerId ? `runner-info-${runnerId}` : null} {executeNumber} />
</WidgetColumnBarItem>
{#if previewReader}
<WidgetColumnBarItem title="Preview" name="preview">
<!-- <PreviewDataGrid reader={previewReader} /> -->
</WidgetColumnBarItem>
{/if}
<WidgetColumnBarItem title="Advanced configuration" name="config" collapsed>
<FormTextField label="Schedule" name="schedule" />
<FormTextField label="Start variable index" name="startVariableIndex" />
</WidgetColumnBarItem>
</WidgetColumnBar>
</svelte:fragment>
</HorizontalSplitter>
</div>
<div class="footer">
<div class="flex m-2">
{#if busy}
<LargeButton icon="icon close" on:click={handleCancel}>Cancel</LargeButton>
{:else}
<LargeFormButton on:click={handleExecute} icon="icon run">Run</LargeFormButton>
{/if}
<LargeFormButton icon="img sql-file" on:click={handleGenerateScript}>Generate script</LargeFormButton>
<LargeButton on:click={closeCurrentModal} icon="icon close">Close</LargeButton>
</div>
</div>
</ModalBase>
</FormProvider>
<style>
.wrapper {
display: flex;
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 100px;
}
.footer {
position: fixed;
height: 100px;
left: 0;
right: 0;
bottom: 0px;
border-top: 1px solid var(--theme-border);
background-color: var(--theme-bg-modalheader);
}
</style>

View File

@ -8,6 +8,8 @@
export let fullScreen = false;
export let noPadding = false;
export let modalId;
export let skipBody = false;
export let skipFooter = false;
function handleCloseModal() {
closeModal(modalId);
@ -31,18 +33,26 @@
<div id="myModal" class="bglayer">
<!-- Modal content -->
<div class="window" class:fullScreen use:clickOutside on:clickOutside={handleCloseModal}>
<div class="header">
<div><slot name="header" /></div>
<div class="close" on:click={handleCloseModal}>
<FontIcon icon="icon close" />
{#if $$slots.header}
<div class="header">
<div><slot name="header" /></div>
<div class="close" on:click={handleCloseModal}>
<FontIcon icon="icon close" />
</div>
</div>
</div>
<div class="content" class:noPadding>
{/if}
{#if !skipBody}
<div class="content" class:noPadding>
<slot />
</div>
{:else}
<slot />
</div>
<div class="footer">
<slot name="footer" />
</div>
{/if}
{#if !skipFooter}
<div class="footer">
<slot name="footer" />
</div>
{/if}
</div>
</div>
@ -63,16 +73,24 @@
.window {
background-color: var(--theme-bg-0);
margin: auto;
margin-top: 15vh;
border: 1px solid var(--theme-border);
width: 50%;
overflow: auto;
outline: none;
}
.window:not(.fullScreen) {
border-radius: 10px;
margin: auto;
margin-top: 15vh;
width: 50%;
}
.window.fullScreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.close {