diff --git a/webapp/packages/core/assets/locales/en.js b/webapp/packages/core/assets/locales/en.js index ba9bf6716e..7544839116 100644 --- a/webapp/packages/core/assets/locales/en.js +++ b/webapp/packages/core/assets/locales/en.js @@ -32,10 +32,15 @@ module.exports = { data_transfer_dialog_export: 'Export', data_transfer_dialog_exporting_object: 'Exporting:', data_transfer_dialog_configuration_title: 'Export configuration', + data_transfer_dialog_preparation: 'We prepare your file for export. Please wait...', + data_transfer_notification_preparation: 'We prepare your file for download. Please wait...', + data_transfer_notification_ready: 'File ready to download', + data_transfer_notification_error: 'Preparation failed or canceled', ui_stepper_back: 'Back', ui_processing_loading: 'Loading...', ui_processing_cancel: 'Cancel', + ui_processing_canceling: 'Canceling...', ui_processing_retry: 'Retry', ui_error_message: 'Error:', ui_errors_details: 'Details', diff --git a/webapp/packages/core/assets/locales/ru.js b/webapp/packages/core/assets/locales/ru.js index f293429a94..5c32d5cdbc 100644 --- a/webapp/packages/core/assets/locales/ru.js +++ b/webapp/packages/core/assets/locales/ru.js @@ -31,10 +31,15 @@ module.exports = { data_transfer_dialog_export: 'Экспортировать', data_transfer_dialog_exporting_object: 'Экспортировать:', data_transfer_dialog_configuration_title: 'Настройка экспорта', + data_transfer_dialog_preparation: 'Подготавливаем файл к экспорту. Пожалуйста подождите...', + data_transfer_notification_preparation: 'Подготавливаем файл к загрузке. Пожалуйста подождите...', + data_transfer_notification_ready: 'Файл готов к загрузке', + data_transfer_notification_error: 'Подготовка файла к загрузке не удалась или была отменена', ui_stepper_back: 'Назад', ui_processing_loading: 'Загрузка...', ui_processing_cancel: 'Отменить', + ui_processing_canceling: 'Отмена...', ui_processing_retry: 'Повторить', ui_error_message: 'Ошибка:', ui_errors_details: 'Информация', diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx index e3bcb5029e..d28d344d09 100644 --- a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx @@ -12,6 +12,7 @@ import styled, { use } from 'reshadow'; import { Icon, Button } from '@dbeaver/core/blocks'; import { ENotificationType } from '@dbeaver/core/eventsLog'; +import { useTranslate } from '@dbeaver/core/localization'; import { useStyles } from '@dbeaver/core/theming'; import { NotificationMark } from './NotificationMark'; @@ -37,6 +38,7 @@ export function Snackbar({ const styles = useStyles(SNACKBAR_STYLES); const [mounted, setMounted] = useState(false); const [closing, setClosing] = useState(false); + const translate = useTranslate(); useEffect(() => { setMounted(true); @@ -78,7 +80,7 @@ export function Snackbar({ onClick={onShowDetails} disabled={disableShowDetails} > - Details + {translate('ui_errors_details')} )} diff --git a/webapp/packages/core/src/blocks/Button.tsx b/webapp/packages/core/src/blocks/Button.tsx index 797e19ea04..6935875401 100644 --- a/webapp/packages/core/src/blocks/Button.tsx +++ b/webapp/packages/core/src/blocks/Button.tsx @@ -12,52 +12,69 @@ import { useStyles, composes } from '@dbeaver/core/theming'; const buttonStyles = composes( css` - button { - composes: theme-button from global; - } - ripple { - composes: theme-button_ripple from global; - } + Button { + composes: theme-button from global; + } + ripple { + composes: theme-button_ripple from global; + } + `, + css` + Button { + display: flex; + } ` ); const buttonMod = { raised: composes( css` - button { + Button { composes: theme-button_raised from global; } ` ), unelevated: composes( css` - button { + Button { composes: theme-button_unelevated from global; } ` ), outlined: composes( css` - button { + Button { composes: theme-button_outlined from global; } ` ), secondary: composes( css` - button { + Button { composes: theme-button_secondary from global; } ` ), }; -type ButtonProps = React.ButtonHTMLAttributes & { +type ButtonProps = ( + React.ButtonHTMLAttributes + & React.LinkHTMLAttributes + ) & { mod?: (keyof typeof buttonMod)[]; + tag?: 'button' | 'a'; + href?: string; + download?: boolean; } -export function Button({ children, mod, ...rest }: ButtonProps) { +export function Button({ + children, + mod, + tag = 'button', + ...rest +}: ButtonProps) { + const Button = tag; return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))( - + ); } diff --git a/webapp/packages/data-export-plugin/src/DataExportService.ts b/webapp/packages/data-export-plugin/src/DataExportService.ts index df3367b77d..5458effd7a 100644 --- a/webapp/packages/data-export-plugin/src/DataExportService.ts +++ b/webapp/packages/data-export-plugin/src/DataExportService.ts @@ -6,7 +6,9 @@ * you may not use this file except in compliance with the License. */ +import { ErrorDetailsDialog } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; import { NotificationService } from '@dbeaver/core/eventsLog'; import { CachedResource, GraphQLService, DataTransferProcessorInfo, DataTransferParameters @@ -15,6 +17,7 @@ import { Deferred } from '@dbeaver/core/utils'; import { ExportFromContainerProcess } from './ExportFromContainerProcess'; import { ExportFromResultsProcess } from './ExportFromResultsProcess'; +import { PendingNotification } from './PendingNotification'; type ProcessorsResourceMetadata = { loaded: boolean; @@ -27,11 +30,12 @@ export class DataExportService { this.refreshProcessorsAsync.bind(this), (_, { loaded }) => loaded ); - private exportProcesses = new Map>(); + readonly exportProcesses = new Map>(); constructor( private graphQLService: GraphQLService, - private notificationService: NotificationService + private notificationService: NotificationService, + private commonDialogService: CommonDialogService, ) { } async cancel(exportId: string) { @@ -54,7 +58,19 @@ export class DataExportService { try { await this.graphQLService.gql.removeDataTransferFile({ dataFileId }); this.exportProcesses.delete(exportId); - } catch (e) { + } catch (exception) { + this.notificationService.logException(exception, 'Error occurred while deleting file'); + } + } + + async showDetails(exportId: string) { + const process = this.exportProcesses.get(exportId); + if (!process) { + return; + } + try { + await this.commonDialogService.open(ErrorDetailsDialog, process.getRejectionReason()); + } finally { } } @@ -68,6 +84,17 @@ export class DataExportService { return; } this.exportProcesses.delete(exportId); + } + + downloadUrl(exportId: string) { + const process = this.exportProcesses.get(exportId); + if (!process) { + return; + } + const dataFileId = process.getPayload(); + if (!dataFileId) { + return; + } return `/dbeaver/data/${dataFileId}`; } @@ -80,9 +107,7 @@ export class DataExportService { const taskId = await process.start(connectionId, containerNodePath, parameters); if (taskId) { this.exportProcesses.set(taskId, process); - // this.notificationService.logInfo({ title: 'We prepare your file for export. Please wait' }); - await process.promise; - window.open(this.download(taskId), '_blank'); + this.showPendingNotification(taskId); return taskId; } } @@ -97,12 +122,19 @@ export class DataExportService { const taskId = await process.start(connectionId, contextId, resultsId, parameters); if (taskId) { this.exportProcesses.set(taskId, process); - await process.promise; - window.open(this.download(taskId), '_blank'); + this.showPendingNotification(taskId); return taskId; } } + private showPendingNotification(taskId: string) { + this.notificationService.logInfo({ + title: 'We prepare your file for export. Please wait', + source: taskId, + customComponent: () => PendingNotification, + }); + } + private async refreshProcessorsAsync( data: Map, metadata: ProcessorsResourceMetadata, diff --git a/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx b/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx index 5b7f952cc8..1e8ea520cf 100644 --- a/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx +++ b/webapp/packages/data-export-plugin/src/Dialog/ProcessorConfigureDialog.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react'; import styled, { css } from 'reshadow'; -import { Loader, IProperty, PropertiesTable } from '@dbeaver/core/blocks'; +import { IProperty, PropertiesTable } from '@dbeaver/core/blocks'; import { CommonDialogWrapper } from '@dbeaver/core/dialogs'; import { useTranslate } from '@dbeaver/core/localization'; import { DataTransferProcessorInfo } from '@dbeaver/core/sdk'; @@ -49,7 +49,8 @@ export const ProcessorConfigureDialog = observer( onBack, onExport, }: ProcessorSelectDialogProps) { - const title = `${useTranslate('data_transfer_dialog_configuration_title')} (${processor.name})`; + const translate = useTranslate(); + const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`; return styled(styles)( - {isExporting && We prepare your file for export. Please wait...} + {isExporting && {translate('data_transfer_dialog_preparation')}} {!isExporting && ( ) { + const translate = useTranslate(); + const dataExportService = useService(DataExportService); + const task = dataExportService.exportProcesses.get(notification.source); + if (!task) { + return null; + } + const [mounted, setMounted] = useState(false); + const [idDetailsOpen, setDetails] = useState(false); + + const getDownloadUrl = useCallback( + () => dataExportService.downloadUrl(notification.source), + [dataExportService, notification] + ); + + const handleDelete = useCallback(() => { + dataExportService.delete(notification.source); + onClose(); + }, [dataExportService, notification, onClose]); + + const handleDownload = useCallback(() => { + dataExportService.download(notification.source); + onClose(); + }, [dataExportService, notification, onClose]); + + const handleCancel = useCallback(() => { + dataExportService.cancel(notification.source); + }, [dataExportService, notification]); + + const handleDetails = useCallback(async () => { + try { + setDetails(true); + await dataExportService.showDetails(notification.source); + } finally { + setDetails(false); + } + }, [dataExportService, notification]); + + useEffect(() => { + setMounted(true); + }, []); + + const status = useCallback(() => { + switch (task.getState()) { + case EDeferredState.PENDING: + return translate('data_transfer_notification_preparation'); + case EDeferredState.CANCELLING: + return translate('ui_processing_canceling'); + case EDeferredState.RESOLVED: + return translate('data_transfer_notification_ready'); + default: + return translate('data_transfer_notification_error'); + } + }, [task]); + + const isSuccess = task.getState() === EDeferredState.RESOLVED; + + return styled(useStyles(SNACKBAR_STYLES, styles))( + + + + {status()} + {task.isFinished && ( + + + + )} + + + + + {task.isFinished && isSuccess && ( + <> + + + + )} + {task.isFinished && !isSuccess && ( + + )} + {!task.isFinished && ( + + )} + + + + ); +});