diff --git a/webapp/packages/core-app/public/icons/export.svg b/webapp/packages/core-app/public/icons/export.svg
new file mode 100644
index 0000000000..9d4f96c274
--- /dev/null
+++ b/webapp/packages/core-app/public/icons/export.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/webapp/packages/core-app/public/icons/import.svg b/webapp/packages/core-app/public/icons/import.svg
new file mode 100644
index 0000000000..34884eee01
--- /dev/null
+++ b/webapp/packages/core-app/public/icons/import.svg
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/webapp/packages/core-blocks/src/UploadButton.tsx b/webapp/packages/core-blocks/src/UploadButton.tsx
new file mode 100644
index 0000000000..c26b6a2415
--- /dev/null
+++ b/webapp/packages/core-blocks/src/UploadButton.tsx
@@ -0,0 +1,34 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 DBeaver Corp and others
+ *
+ * Licensed under the Apache License, Version 2.0.
+ * you may not use this file except in compliance with the License.
+ */
+
+import styled, { css } from 'reshadow';
+
+import { uuid } from '@cloudbeaver/core-utils';
+
+interface Props extends React.DetailedHTMLProps, HTMLInputElement> {
+ className?: string;
+}
+
+const styles = css`
+ label {
+ cursor: pointer;
+ }
+`;
+
+export const UploadButton: React.FC = function UploadButton({ id, className, children, ...rest }) {
+ const _id = id ?? uuid();
+
+ return styled(styles)(
+ <>
+
+
+ >
+ );
+};
diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts
index 77427b7da7..b14967984d 100644
--- a/webapp/packages/core-blocks/src/index.ts
+++ b/webapp/packages/core-blocks/src/index.ts
@@ -125,6 +125,7 @@ export * from './FormControls/SubmittingForm';
export * from './FormControls/Textarea';
export * from './Link';
export * from './Cell';
+export * from './UploadButton';
export * from './ErrorMessage';
export * from './StatusMessage';
export * from './ExceptionMessage';
diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts
index f862cf9563..5ce70e28ad 100644
--- a/webapp/packages/core-localization/src/locales/en.ts
+++ b/webapp/packages/core-localization/src/locales/en.ts
@@ -52,6 +52,7 @@ export const defaultENLocale = [
['ui_set', 'Set'],
['ui_are_you_sure', 'Are you sure?'],
['ui_options', 'Options'],
+ ['ui_changes_might_be_lost', 'Your changes might be lost'],
['root_permission_denied', "You don't have permissions"],
['root_permission_no_permission', "You don't have permission for this action"],
diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts
index 2287f4b8bd..0e364814c7 100644
--- a/webapp/packages/core-localization/src/locales/ru.ts
+++ b/webapp/packages/core-localization/src/locales/ru.ts
@@ -50,6 +50,7 @@ export const defaultRULocale = [
['ui_set', 'Установить'],
['ui_are_you_sure', 'Вы уверены?'],
['ui_options', 'Параметры'],
+ ['ui_changes_might_be_lost', 'Ваши изменения могут быть потеряны'],
['root_permission_denied', 'Отказано в доступе'],
['root_permission_no_permission', 'У вас нет разрешения на это действие'],
diff --git a/webapp/packages/core-utils/src/download.ts b/webapp/packages/core-utils/src/download.ts
new file mode 100644
index 0000000000..6022c41fe2
--- /dev/null
+++ b/webapp/packages/core-utils/src/download.ts
@@ -0,0 +1,27 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 DBeaver Corp and others
+ *
+ * Licensed under the Apache License, Version 2.0.
+ * you may not use this file except in compliance with the License.
+ */
+
+export function download(blob: Blob, fileName: string): void {
+ const saveLink = document.createElement('a');
+
+ saveLink.download = fileName;
+ saveLink.style.display = 'none';
+ document.body.appendChild(saveLink);
+
+ try {
+ const url = URL.createObjectURL(blob);
+ saveLink.href = url;
+ saveLink.onclick = () => requestAnimationFrame(() => URL.revokeObjectURL(url));
+ } catch (e) {
+ console.error(e);
+ console.warn('Error while getting object URL.');
+ }
+
+ saveLink.click();
+ document.body.removeChild(saveLink);
+}
diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts
index 7e34843ba6..8fc6aa5bb4 100644
--- a/webapp/packages/core-utils/src/index.ts
+++ b/webapp/packages/core-utils/src/index.ts
@@ -27,3 +27,5 @@ export * from './getCookies';
export * from './getUniqueName';
export * from './isMapsEqual';
export * from './openCenteredPopup';
+export * from './download';
+export * from './uploadTextFiles';
diff --git a/webapp/packages/core-utils/src/uploadTextFiles.ts b/webapp/packages/core-utils/src/uploadTextFiles.ts
new file mode 100644
index 0000000000..1d2b98838c
--- /dev/null
+++ b/webapp/packages/core-utils/src/uploadTextFiles.ts
@@ -0,0 +1,52 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 DBeaver Corp and others
+ *
+ * Licensed under the Apache License, Version 2.0.
+ * you may not use this file except in compliance with the License.
+ */
+
+interface UploadProcess {
+ promise: Promise;
+ reader: FileReader;
+}
+
+type UploadData = Map;
+
+export function uploadTextFiles(files: File[]): UploadData {
+ const result: UploadData = new Map();
+
+ for (const file of files) {
+ const reader = new FileReader();
+
+ const promise: Promise = new Promise((resolve, reject) => {
+ reader.readAsText(file);
+
+ reader.onload = event => {
+ const data = event.target?.result;
+
+ if (data) {
+ resolve(data as string);
+ } else {
+ reject(new Error(`No data for the file: "${file.name}"`));
+ }
+ };
+
+ reader.onerror = event => {
+ reject(new Error(`Error occurred reading file: "${file.name}"`));
+ if (event.target?.error) {
+ console.error(event.target.error);
+ }
+ };
+
+ reader.onabort = () => reject(new Error(`Reading "${file.name}" is aborted`));
+ });
+
+ result.set(file, {
+ promise,
+ reader,
+ });
+ }
+
+ return result;
+}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx
index 59db56805a..7da205a5d9 100644
--- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { useEffect, useRef } from 'react';
import styled, { css } from 'reshadow';
-import { StaticImage, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
+import { StaticImage, UploadButton, useTab as useBaseTab } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
@@ -19,10 +19,11 @@ import type { ISqlEditorProps } from './ISqlEditorProps';
import type { SQLCodeEditorController } from './SQLCodeEditor/SQLCodeEditorController';
import { SQLCodeEditorLoader } from './SQLCodeEditor/SQLCodeEditorLoader';
import { SqlEditorController } from './SqlEditorController';
+import { useTools } from './useTools';
const styles = composes(
css`
- button {
+ button, upload {
composes: theme-ripple from global;
}
`,
@@ -35,6 +36,12 @@ const styles = composes(
display: flex;
overflow: auto;
}
+
+ container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
actions {
width: 32px;
@@ -44,8 +51,15 @@ const styles = composes(
align-items: center;
user-select: none;
}
+
+ tools {
+ width: 32px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
- button {
+ button, upload {
background: none;
padding: 0;
margin: 0;
@@ -76,6 +90,7 @@ export const SqlEditor = observer(function SqlEditor({ tab, cla
const editor = useRef(null);
const baseTab = useBaseTab(tab.id);
const controller = useController(SqlEditorController, tab);
+ const tools = useTools(controller, tab);
useEffect(() => {
if (!baseTab.selected) {
@@ -95,38 +110,58 @@ export const SqlEditor = observer(function SqlEditor({ tab, cla
return styled(style)(
-
-
-
-
- {controller.dialect?.supportsExplainExecutionPlan && (
+
+
- )}
-
+
+
+ {controller.dialect?.supportsExplainExecutionPlan && (
+
+ )}
+
+
+
+ tools.uploadScript(event.target.files)}
+ >
+
+
+
+
+
+
Promise
@@ -515,8 +520,8 @@ export class SqlEditorController implements IInitializableController, IDestructi
(data as any).cancel(); // seems it doesn't works, after disabling read-only mode, typings appears
return;
}
- this.tab.handlerState.query = query;
- this.parser.setScript(query);
+
+ this.setQuery(query);
}
}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx
new file mode 100644
index 0000000000..d2c5fabc6e
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx
@@ -0,0 +1,120 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2020-2021 DBeaver Corp and others
+ *
+ * Licensed under the Apache License, Version 2.0.
+ * you may not use this file except in compliance with the License.
+ */
+
+import { action } from 'mobx';
+
+import type { ITab } from '@cloudbeaver/core-app';
+import { useObservableRef } from '@cloudbeaver/core-blocks';
+import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
+import { useService } from '@cloudbeaver/core-di';
+import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
+import { NotificationService } from '@cloudbeaver/core-events';
+import { download, generateFileName, uploadTextFiles } from '@cloudbeaver/core-utils';
+
+import type { ISqlEditorTabState } from '../ISqlEditorTabState';
+import { SqlEditorSettingsService } from '../SqlEditorSettingsService';
+import type { SqlEditorController } from './SqlEditorController';
+
+interface State {
+ uploadScript: (files: FileList | null) => Promise;
+ downloadScript: () => void;
+}
+
+export function useTools(controller: SqlEditorController, tab: ITab): Readonly {
+ const commonDialogService = useService(CommonDialogService);
+ const notificationService = useService(NotificationService);
+ const connectionInfoResource = useService(ConnectionInfoResource);
+ const sqlEditorSettingsService = useService(SqlEditorSettingsService);
+
+ return useObservableRef(() => ({
+ async uploadScript(fileList: FileList | null) {
+ if (!fileList) {
+ throw new Error('No files found');
+ }
+
+ if (!fileList.length) {
+ return;
+ }
+
+ if (this.controller.value.trim()) {
+ const state = await this.commonDialogService.open(ConfirmationDialog, {
+ title: 'ui_changes_might_be_lost',
+ message: 'sql_editor_upload_script_unsaved_changes_dialog_message',
+ confirmActionText: 'ui_processing_save',
+ });
+
+ if (state !== DialogueStateResult.Rejected) {
+ this.downloadScript();
+ }
+ }
+
+ const maxSize = this.sqlEditorSettingsService.settings.getValue('maxFileSize');
+ const aboveMaxSize: File[] = [];
+
+ const files = Array.from(fileList).filter(file => {
+ const size = Math.round(file.size / 1000); // kilobyte
+ const above = size > maxSize;
+
+ if (above) {
+ aboveMaxSize.push(file);
+ }
+
+ return !above;
+ });
+
+ if (aboveMaxSize.length > 0) {
+ const nameList = aboveMaxSize.map(file => `${file.name} (${Math.round(file.size / 1000)}KB)`);
+ const message = `Max size: ${maxSize}KB\nFollowing files exceed max size: "${nameList.join(', ')}"`;
+
+ this.notificationService.logInfo({
+ title: 'sql_editor_upload_script_max_size_title',
+ message,
+ persistent: true,
+ });
+ }
+
+ if (!files.length) {
+ return;
+ }
+
+ const data = uploadTextFiles(files);
+ const process = Array.from(data.values())[0];
+
+ try {
+ const script = await process.promise;
+ controller.setQuery(script);
+ } catch (exception) {
+ this.notificationService.logException(exception, 'Uploading file error');
+ }
+ },
+
+ downloadScript() {
+ const script = this.controller.value;
+
+ if (!script.trim()) {
+ return;
+ }
+
+ const blob = new Blob([script], {
+ type: 'application/sql',
+ });
+
+ let name = 'script';
+ const connectionId = this.tab.handlerState.executionContext?.connectionId;
+
+ if (connectionId) {
+ const connection = this.connectionInfoResource.get(connectionId);
+ name = connection ? `${connection.name}-script` : name;
+ }
+
+ download(blob, generateFileName(name, '.sql'));
+ },
+ }),
+ { uploadScript: action.bound, downloadScript: action.bound },
+ { controller, commonDialogService, connectionInfoResource, notificationService, sqlEditorSettingsService, tab });
+}
diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts
new file mode 100644
index 0000000000..d02ad79a62
--- /dev/null
+++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts
@@ -0,0 +1,30 @@
+/*
+ * CloudBeaver - Cloud Database Manager
+ * Copyright (C) 2019-2021 DBeaver Corp
+ *
+ * All Rights Reserved
+ *
+ * NOTICE: All information contained herein is, and remains
+ * the property of DBeaver Corp and its suppliers, if any.
+ * The intellectual and technical concepts contained
+ * herein are proprietary to DBeaver Corp and its suppliers
+ * and may be covered by U.S. and Foreign Patents,
+ * patents in process, and are protected by trade secret or copyright law.
+ * Dissemination of this information or reproduction of this material
+ * is strictly forbidden unless prior written permission is obtained
+ * from DBeaver Corp.
+ */
+
+import { injectable } from '@cloudbeaver/core-di';
+import { PluginManagerService } from '@cloudbeaver/core-plugin';
+
+const defaultSettings = {
+ maxFileSize: 100, // kilobyte
+};
+
+@injectable()
+export class SqlEditorSettingsService {
+ readonly settings = this.pluginManagerService.getPluginSettings('core.app.sqlEditor', defaultSettings);
+
+ constructor(private pluginManagerService: PluginManagerService) { }
+}
diff --git a/webapp/packages/plugin-sql-editor/src/locales/en.ts b/webapp/packages/plugin-sql-editor/src/locales/en.ts
index e42b89c16b..0a95df73fa 100644
--- a/webapp/packages/plugin-sql-editor/src/locales/en.ts
+++ b/webapp/packages/plugin-sql-editor/src/locales/en.ts
@@ -14,4 +14,8 @@ export default [
['sql_execution_plan_properties_panel_value', 'Value'],
['sql_execution_plan_properties_panel_general', 'General'],
['sql_execution_plan_properties_panel_details', 'Details'],
+ ['sql_editor_upload_script_tooltip', 'Upload sql script'],
+ ['sql_editor_download_script_tooltip', 'Download sql script'],
+ ['sql_editor_upload_script_unsaved_changes_dialog_message', 'Your current script will be overwritten by the uploaded script. Do you want to save it first?'],
+ ['sql_editor_upload_script_max_size_title', 'File size exceeds max size'],
];
diff --git a/webapp/packages/plugin-sql-editor/src/locales/ru.ts b/webapp/packages/plugin-sql-editor/src/locales/ru.ts
index 4c136f790d..54fd6d2dc3 100644
--- a/webapp/packages/plugin-sql-editor/src/locales/ru.ts
+++ b/webapp/packages/plugin-sql-editor/src/locales/ru.ts
@@ -14,4 +14,8 @@ export default [
['sql_execution_plan_properties_panel_value', 'Значение'],
['sql_execution_plan_properties_panel_general', 'Общие'],
['sql_execution_plan_properties_panel_details', 'Дополнительные'],
+ ['sql_editor_upload_script_tooltip', 'Загрузить sql скрипт'],
+ ['sql_editor_download_script_tooltip', 'Скачать sql скрипт'],
+ ['sql_editor_upload_script_unsaved_changes_dialog_message', 'Текущий скрипт будет перезаписан загруженным. Сохранить текущий?'],
+ ['sql_editor_upload_script_max_size_title', 'Размер файла превышает максимально допустимый'],
];
diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts
index ad8f2c82f7..1aed1fce6f 100644
--- a/webapp/packages/plugin-sql-editor/src/manifest.ts
+++ b/webapp/packages/plugin-sql-editor/src/manifest.ts
@@ -13,6 +13,7 @@ import { SqlDialectInfoService } from './SqlDialectInfoService';
import { SqlEditorBootstrap } from './SqlEditorBootstrap';
import { SqlEditorNavigatorService } from './SqlEditorNavigatorService';
import { SqlEditorService } from './SqlEditorService';
+import { SqlEditorSettingsService } from './SqlEditorSettingsService';
import { SqlEditorTabService } from './SqlEditorTabService';
import { SqlExecutionPlanService } from './SqlResultTabs/ExecutionPlan/SqlExecutionPlanService';
import { SqlQueryResultService } from './SqlResultTabs/SqlQueryResultService';
@@ -35,5 +36,6 @@ export const sqlEditorPluginManifest: PluginManifest = {
SqlEditorService,
SqlEditorNavigatorService,
LocaleService,
+ SqlEditorSettingsService,
],
};
diff --git a/webapp/packages/product-default/src/config.json5 b/webapp/packages/product-default/src/config.json5
index 30531e5afc..10ec9e00f8 100644
--- a/webapp/packages/product-default/src/config.json5
+++ b/webapp/packages/product-default/src/config.json5
@@ -16,6 +16,10 @@
maxLogRecords: 2000,
maxFailedRequests: 3
},
+ sqlEditor: {
+ // max size of the file that can be uploaded to the editor (in kilobytes)
+ maxFileSize: 100,
+ }
}
},
plugin_data_spreadsheet_new: {