From a455bf717c80e042c4424701db3f32bb1817b247 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 1 Nov 2021 20:04:54 +0300 Subject: [PATCH] feat(plugin-sql-editor): CB-1083 upload and download script option --- .../packages/core-app/public/icons/export.svg | 14 ++ .../packages/core-app/public/icons/import.svg | 10 ++ .../packages/core-blocks/src/UploadButton.tsx | 34 +++++ webapp/packages/core-blocks/src/index.ts | 1 + .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/ru.ts | 1 + webapp/packages/core-utils/src/download.ts | 27 ++++ webapp/packages/core-utils/src/index.ts | 2 + .../core-utils/src/uploadTextFiles.ts | 52 ++++++++ .../src/SqlEditor/SqlEditor.tsx | 97 +++++++++----- .../src/SqlEditor/SqlEditorController.tsx | 9 +- .../src/SqlEditor/useTools.tsx | 120 ++++++++++++++++++ .../src/SqlEditorSettingsService.ts | 30 +++++ .../plugin-sql-editor/src/locales/en.ts | 4 + .../plugin-sql-editor/src/locales/ru.ts | 4 + .../plugin-sql-editor/src/manifest.ts | 2 + .../packages/product-default/src/config.json5 | 4 + 17 files changed, 379 insertions(+), 33 deletions(-) create mode 100644 webapp/packages/core-app/public/icons/export.svg create mode 100644 webapp/packages/core-app/public/icons/import.svg create mode 100644 webapp/packages/core-blocks/src/UploadButton.tsx create mode 100644 webapp/packages/core-utils/src/download.ts create mode 100644 webapp/packages/core-utils/src/uploadTextFiles.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts 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 @@ + + + export + + + + + + + + + + + \ 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 @@ + + + Created with Sketch. + + + + + + + \ 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: {