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} +
    + {#each buttons as button} +
    + +
    + {/each} +
    + {/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, };