diff --git a/packages/api/src/shell/index.js b/packages/api/src/shell/index.js
index fe9652579..905c3e6ea 100644
--- a/packages/api/src/shell/index.js
+++ b/packages/api/src/shell/index.js
@@ -6,6 +6,7 @@ const copyStream = require('./copyStream');
const fakeObjectReader = require('./fakeObjectReader');
const consoleObjectWriter = require('./consoleObjectWriter');
const jsonLinesWriter = require('./jsonLinesWriter');
+const jsonArrayWriter = require('./jsonArrayWriter');
const jsonLinesReader = require('./jsonLinesReader');
const jslDataReader = require('./jslDataReader');
const archiveWriter = require('./archiveWriter');
@@ -26,6 +27,7 @@ const dbgateApi = {
tableReader,
copyStream,
jsonLinesWriter,
+ jsonArrayWriter,
jsonLinesReader,
fakeObjectReader,
consoleObjectWriter,
diff --git a/packages/api/src/shell/jsonArrayWriter.js b/packages/api/src/shell/jsonArrayWriter.js
new file mode 100644
index 000000000..3e631b30f
--- /dev/null
+++ b/packages/api/src/shell/jsonArrayWriter.js
@@ -0,0 +1,52 @@
+const fs = require('fs');
+const stream = require('stream');
+
+class StringifyStream extends stream.Transform {
+ constructor() {
+ super({ objectMode: true });
+ this.wasHeader = false;
+ this.wasRecord = false;
+ }
+ _transform(chunk, encoding, done) {
+ let skip = false;
+
+ if (!this.wasHeader) {
+ skip =
+ chunk.__isStreamHeader ||
+ // TODO remove isArray test
+ Array.isArray(chunk.columns);
+ this.wasHeader = true;
+ }
+ if (!skip) {
+ if (!this.wasRecord) {
+ this.push('[\n');
+ } else {
+ this.push(',\n');
+ }
+ this.wasRecord = true;
+
+ this.push(JSON.stringify(chunk));
+ }
+ done();
+ }
+
+ _flush(done) {
+ if (!this.wasRecord) {
+ this.push('[]\n');
+ } else {
+ this.push('\n]\n');
+ }
+ done();
+ }
+}
+
+async function jsonArrayWriter({ fileName, encoding = 'utf-8' }) {
+ console.log(`Writing file ${fileName}`);
+ const stringify = new StringifyStream();
+ const fileStream = fs.createWriteStream(fileName, encoding);
+ stringify.pipe(fileStream);
+ stringify['finisher'] = fileStream;
+ return stringify;
+}
+
+module.exports = jsonArrayWriter;
diff --git a/packages/types/extensions.d.ts b/packages/types/extensions.d.ts
index 142b6658a..fca442fe9 100644
--- a/packages/types/extensions.d.ts
+++ b/packages/types/extensions.d.ts
@@ -33,9 +33,16 @@ export interface PluginDefinition {
content: any;
}
+export interface QuickExportDefinition {
+ label: string;
+ createWriter: (fileName: string) => { functionName: string; props: any };
+ extension: string;
+}
+
export interface ExtensionsDirectory {
plugins: PluginDefinition[];
fileFormats: FileFormatDefinition[];
+ quickExports: QuickExportDefinition[];
drivers: EngineDriver[];
themes: ThemeDefinition[];
}
diff --git a/packages/web/src/Screen.svelte b/packages/web/src/Screen.svelte
index 3d0ab19eb..0b15333ab 100644
--- a/packages/web/src/Screen.svelte
+++ b/packages/web/src/Screen.svelte
@@ -6,6 +6,7 @@
currentThemeDefinition,
isFileDragActive,
leftPanelWidth,
+ openedSnackbars,
selectedWidget,
visibleCommandPalette,
visibleToolbar,
@@ -17,11 +18,13 @@
import splitterDrag from './utility/splitterDrag';
import CurrentDropDownMenu from './modals/CurrentDropDownMenu.svelte';
import StatusBar from './widgets/StatusBar.svelte';
+ import Snackbar from './widgets/Snackbar.svelte';
import ModalLayer from './modals/ModalLayer.svelte';
import DragAndDropFileTarget from './DragAndDropFileTarget.svelte';
import dragDropFileTarget from './utility/dragDropFileTarget';
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
+
@@ -64,6 +67,11 @@
{#if $isFileDragActive}
{/if}
+
+ {#each $openedSnackbars as snackbar(snackbar.id)}
+
+ {/each}
+
diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte
index c1da6219c..35cd5f07b 100644
--- a/packages/web/src/appobj/ArchiveFileAppObject.svelte
+++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte
@@ -14,14 +14,19 @@
export const extractKey = data => data.fileName;
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
+
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
export const createMatcher = ({ pureName }) => filter => filterName(filter, pureName);
+ const electron = getElectron();
const icons = {
tables: 'img table',
@@ -46,6 +47,10 @@
{
divider: true,
},
+ {
+ isQuickExport: true,
+ functionName: 'tableReader',
+ },
{
label: 'Export',
isExport: true,
@@ -108,6 +113,10 @@
{
divider: true,
},
+ {
+ isQuickExport: true,
+ functionName: 'tableReader',
+ },
{
label: 'Export',
isExport: true,
@@ -165,6 +174,10 @@
{
divider: true,
},
+ {
+ isQuickExport: true,
+ functionName: 'tableReader',
+ },
{
label: 'Export',
isExport: true,
@@ -261,6 +274,10 @@
},
},
},
+ {
+ isQuickExport: true,
+ functionName: 'tableReader',
+ },
{
label: 'Export',
isExport: true,
@@ -311,6 +328,7 @@
{ forceNewTab }
);
}
+
- dispatch('close')}
- bind:this={element}
->
+
+{#if submenuItem?.submenu}
+ {
+ if (onCloseParent) onCloseParent();
+ dispatch('close');
+ }}
+ />
+{/if}
diff --git a/packages/web/src/plugins/PluginsProvider.svelte b/packages/web/src/plugins/PluginsProvider.svelte
index a6ebe00a1..c6d30dd61 100644
--- a/packages/web/src/plugins/PluginsProvider.svelte
+++ b/packages/web/src/plugins/PluginsProvider.svelte
@@ -40,7 +40,6 @@
function buildDrivers(plugins) {
const res = [];
for (const { content } of plugins) {
- // if (content.driver) res.push(content.driver);
if (content.drivers) res.push(...content.drivers);
}
return res;
@@ -52,6 +51,7 @@
fileFormats: buildFileFormats(plugins),
themes: buildThemes(plugins),
drivers: buildDrivers(plugins),
+ quickExports: buildQuickExports(plugins),
};
return extensions;
}
@@ -63,7 +63,7 @@
import { extensions, loadingPluginStore } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import { useInstalledPlugins } from '../utility/metadataLoaders';
- import { buildFileFormats } from './fileformats';
+ import { buildFileFormats, buildQuickExports } from './fileformats';
import { buildThemes } from './themes';
import dbgateTools from 'dbgate-tools';
diff --git a/packages/web/src/plugins/fileformats.ts b/packages/web/src/plugins/fileformats.ts
index 2f8fddf86..6f91fa086 100644
--- a/packages/web/src/plugins/fileformats.ts
+++ b/packages/web/src/plugins/fileformats.ts
@@ -1,4 +1,4 @@
-import { FileFormatDefinition } from 'dbgate-types';
+import { FileFormatDefinition, QuickExportDefinition } from 'dbgate-types';
const jsonlFormat = {
storageType: 'jsonl',
@@ -8,8 +8,37 @@ const jsonlFormat = {
writerFunc: 'jsonLinesWriter',
};
+const jsonFormat = {
+ storageType: 'json',
+ extension: 'json',
+ name: 'JSON',
+ writerFunc: 'jsonArrayWriter',
+};
+
+const jsonlQuickExport = {
+ label: 'JSON lines',
+ extension: 'jsonl',
+ createWriter: fileName => ({
+ functionName: 'jsonLinesWriter',
+ props: {
+ fileName,
+ },
+ }),
+};
+
+const jsonQuickExport = {
+ label: 'JSON',
+ extension: 'json',
+ createWriter: fileName => ({
+ functionName: 'jsonArrayWriter',
+ props: {
+ fileName,
+ },
+ }),
+};
+
export function buildFileFormats(plugins): FileFormatDefinition[] {
- const res = [jsonlFormat];
+ const res = [jsonlFormat, jsonFormat];
for (const { content } of plugins) {
const { fileFormats } = content;
if (fileFormats) res.push(...fileFormats);
@@ -17,6 +46,14 @@ export function buildFileFormats(plugins): FileFormatDefinition[] {
return res;
}
+export function buildQuickExports(plugins): QuickExportDefinition[] {
+ const res = [jsonQuickExport, jsonlQuickExport];
+ for (const { content } of plugins) {
+ if (content.quickExports) res.push(...content.quickExports);
+ }
+ return res;
+}
+
export function findFileFormat(extensions, storageType) {
return extensions.fileFormats.find(x => x.storageType == storageType);
}
diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts
index ad676cfd9..a0754b624 100644
--- a/packages/web/src/stores.ts
+++ b/packages/web/src/stores.ts
@@ -55,6 +55,7 @@ export const visibleToolbar = writableWithStorage(true, 'visibleToolbar');
export const leftPanelWidth = writable(300);
export const currentDropDownMenu = writable(null);
export const openedModals = writable([]);
+export const openedSnackbars = writable([]);
export const nullStore = readable(null, () => {});
export const currentArchive = writable('default');
export const isFileDragActive = writable(false);
diff --git a/packages/web/src/tabs/ShellTab.svelte b/packages/web/src/tabs/ShellTab.svelte
index 42ffb46bc..0e18e0bef 100644
--- a/packages/web/src/tabs/ShellTab.svelte
+++ b/packages/web/src/tabs/ShellTab.svelte
@@ -14,24 +14,37 @@
findReplace: true,
});
+ // registerCommand({
+ // id: 'shell.openWizard',
+ // category: 'Shell',
+ // name: 'Open wizard',
+ // // testEnabled: () => getCurrentEditor()?.openWizardEnabled(),
+ // onClick: () => getCurrentEditor().openWizard(),
+ // });
+
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;
+
diff --git a/packages/web/src/tabs/TableDataTab.svelte b/packages/web/src/tabs/TableDataTab.svelte
index 41efdb6e9..ebff70b99 100644
--- a/packages/web/src/tabs/TableDataTab.svelte
+++ b/packages/web/src/tabs/TableDataTab.svelte
@@ -16,6 +16,7 @@
export const matchingProps = ['conid', 'database', 'schemaName', 'pureName'];
export const allowAddToFavorites = props => true;
+
Function
+) {
+ const electron = getElectron();
+ if (!electron) {
+ return { _skip: true };
+ }
+ return {
+ text: 'Quick export',
+ submenu: extensions.quickExports.map(fmt => ({
+ text: fmt.label,
+ onClick: handler(fmt),
+ })),
+ };
+}
diff --git a/packages/web/src/utility/exportElectronFile.ts b/packages/web/src/utility/exportElectronFile.ts
new file mode 100644
index 000000000..2bf4caefa
--- /dev/null
+++ b/packages/web/src/utility/exportElectronFile.ts
@@ -0,0 +1,56 @@
+import ScriptWriter from '../impexp/ScriptWriter';
+import getElectron from './getElectron';
+import axiosInstance from '../utility/axiosInstance';
+import socket from '../utility/socket';
+import { showSnackbar, showSnackbarInfo, showSnackbarError, closeSnackbar } from '../utility/snackbar';
+
+export async function exportElectronFile(dataName, reader, format) {
+ const electron = getElectron();
+ const filters = [{ name: format.label, extensions: [format.extension] }];
+
+ const filePath = electron.remote.dialog.showSaveDialogSync(electron.remote.getCurrentWindow(), {
+ filters,
+ defaultPath: `${dataName}.${format.extension}`,
+ properties: ['showOverwriteConfirmation'],
+ });
+ if (!filePath) return;
+
+ const script = new ScriptWriter();
+
+ const sourceVar = script.allocVariable();
+ script.assign(sourceVar, reader.functionName, reader.props);
+
+ const targetVar = script.allocVariable();
+ const writer = format.createWriter(filePath, dataName);
+ script.assign(targetVar, writer.functionName, writer.props);
+
+ script.copyStream(sourceVar, targetVar);
+ script.put();
+
+ const resp = await axiosInstance.post('runners/start', { script: script.getScript() });
+ const runid = resp.data.runid;
+ let isCanceled = false;
+
+ const snackId = showSnackbar({
+ message: `Exporting ${dataName}`,
+ icon: 'icon loading',
+ buttons: [
+ {
+ label: 'Cancel',
+ onClick: () => {
+ isCanceled = true;
+ axiosInstance.post('runners/cancel', { runid });
+ },
+ },
+ ],
+ });
+
+ function handleRunnerDone() {
+ closeSnackbar(snackId);
+ socket.off(`runner-done-${runid}`, handleRunnerDone);
+ if (isCanceled) showSnackbarError(`Export ${dataName} canceled`);
+ else showSnackbarInfo(`Export ${dataName} finished`);
+ }
+
+ socket.on(`runner-done-${runid}`, handleRunnerDone);
+}
diff --git a/packages/web/src/utility/snackbar.ts b/packages/web/src/utility/snackbar.ts
new file mode 100644
index 000000000..95513c6cc
--- /dev/null
+++ b/packages/web/src/utility/snackbar.ts
@@ -0,0 +1,75 @@
+import { openedSnackbars } from '../stores';
+
+export interface SnackbarButton {
+ label: string;
+ onClick: Function;
+}
+
+export interface SnackbarInfo {
+ message: string;
+ icon?: string;
+ autoClose?: boolean;
+ allowClose?: boolean;
+ buttons?: SnackbarButton[];
+}
+
+let lastSnackbarId = 0;
+
+export function showSnackbar(snackbar: SnackbarInfo): string {
+ lastSnackbarId += 1;
+ const id = lastSnackbarId.toString();
+ openedSnackbars.update(x => [
+ ...x,
+ {
+ ...snackbar,
+ id,
+ },
+ ]);
+ return id;
+}
+
+export function showSnackbarSuccess(message: string) {
+ showSnackbar({
+ message,
+ icon: 'img ok',
+ allowClose: true,
+ autoClose: true,
+ });
+}
+
+export function showSnackbarInfo(message: string) {
+ showSnackbar({
+ message,
+ icon: 'img info',
+ allowClose: true,
+ autoClose: true,
+ });
+}
+
+export function showSnackbarError(message: string) {
+ showSnackbar({
+ message,
+ icon: 'img error',
+ allowClose: true,
+ autoClose: true,
+ });
+}
+
+export function closeSnackbar(snackId: string) {
+ openedSnackbars.update(x => x.filter(x => x.id != snackId));
+}
+// showSnackbar({
+// icon: 'img ok',
+// message: 'Test snackbar',
+// allowClose: true,
+// });
+showSnackbar({
+ icon: 'img ok',
+ message: 'Auto close',
+ autoClose: true,
+});
+// showSnackbar({
+// icon: 'img warn',
+// message: 'Buttons',
+// buttons: [{ label: 'OK', onClick: () => console.log('OK') }],
+// });
diff --git a/packages/web/src/widgets/Snackbar.svelte b/packages/web/src/widgets/Snackbar.svelte
new file mode 100644
index 000000000..0315b7393
--- /dev/null
+++ b/packages/web/src/widgets/Snackbar.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+
+ {message}
+
+
+ {#if allowClose}
+
+
+
+ {/if}
+
+ {#if buttons?.length > 0}
+
+ {/if}
+
+
+
diff --git a/plugins/dbgate-plugin-csv/src/frontend/index.js b/plugins/dbgate-plugin-csv/src/frontend/index.js
index 33b0f6a17..f9e871e5d 100644
--- a/plugins/dbgate-plugin-csv/src/frontend/index.js
+++ b/plugins/dbgate-plugin-csv/src/frontend/index.js
@@ -43,4 +43,29 @@ const fileFormat = {
export default {
fileFormats: [fileFormat],
+
+ quickExports: [
+ {
+ label: 'CSV file',
+ extension: 'csv',
+ createWriter: (fileName) => ({
+ functionName: 'writer@dbgate-plugin-csv',
+ props: {
+ fileName,
+ delimiter: ',',
+ },
+ }),
+ },
+ {
+ label: 'CSV file (semicolor separated)',
+ extension: 'csv',
+ createWriter: (fileName) => ({
+ functionName: 'writer@dbgate-plugin-csv',
+ props: {
+ fileName,
+ delimiter: ';',
+ },
+ }),
+ },
+ ],
};
diff --git a/plugins/dbgate-plugin-excel/src/frontend/index.js b/plugins/dbgate-plugin-excel/src/frontend/index.js
index 94d515376..25ceab8b0 100644
--- a/plugins/dbgate-plugin-excel/src/frontend/index.js
+++ b/plugins/dbgate-plugin-excel/src/frontend/index.js
@@ -64,5 +64,18 @@ const fileFormat = {
export default {
fileFormats: [fileFormat],
+ quickExports: [
+ {
+ label: 'MS Excel',
+ extension: 'xlsx',
+ createWriter: (fileName, dataName) => ({
+ functionName: 'writer@dbgate-plugin-excel',
+ props: {
+ fileName,
+ sheetName: dataName,
+ },
+ }),
+ },
+ ],
initialize,
};