feat(data-export-plugin): add export status notify

This commit is contained in:
Wroud
2020-05-04 16:14:08 +03:00
parent d73c4498f2
commit 7f56d87104
7 changed files with 249 additions and 25 deletions
@@ -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',
@@ -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: 'Информация',
@@ -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')}
</Button>
</actions>
)}
+30 -13
View File
@@ -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<HTMLButtonElement> & {
type ButtonProps = (
React.ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement>
& React.LinkHTMLAttributes<HTMLLinkElement | HTMLButtonElement>
) & {
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])))(
<button {...rest}><ripple as="div"/>{children}</button>
<Button {...rest}><ripple as="div"/>{children}</Button>
);
}
@@ -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<string, Deferred<string>>();
readonly exportProcesses = new Map<string, Deferred<string>>();
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<string, DataTransferProcessorInfo>,
metadata: ProcessorsResourceMetadata,
@@ -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)(
<CommonDialogWrapper
@@ -65,7 +66,7 @@ export const ProcessorConfigureDialog = observer(
}
onReject={onClose}
>
{isExporting && <message as="div">We prepare your file for export. Please wait...</message>}
{isExporting && <message as="div">{translate('data_transfer_dialog_preparation')}</message>}
{!isExporting && (
<PropertiesTable
properties={properties}
@@ -0,0 +1,162 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 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 { observer } from 'mobx-react';
import {
useEffect, useState, useCallback
} from 'react';
import { Button as ReakitButton } from 'reakit/Button';
import styled, { use, css } from 'reshadow';
import { SNACKBAR_STYLES } from '@dbeaver/core/app';
import { Icon, Button, Loader } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { NotificationComponentProps } from '@dbeaver/core/eventsLog';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
import { EDeferredState } from '@dbeaver/core/utils';
import { DataExportService } from './DataExportService';
const styles = css`
Loader {
margin-right: 16px;
}
actions {
display: flex;
}
fill {
flex: 1;
}
Button {
margin-left: 16px !important;
}
actions Button {
display: flex;
}
`;
export const PendingNotification = observer(function PendingNotification({
notification,
onClose,
}: NotificationComponentProps<string>) {
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))(
<notification as="div" {...use({ mounted })} >
<notification-header as="div">
<Loader loading={task.isInProgress} hideMessage />
<message as="div">{status()}</message>
{task.isFinished && (
<ReakitButton onClick={handleDelete}>
<Icon name="cross" viewBox="0 0 16 16" />
</ReakitButton>
)}
</notification-header>
<notification-body as="div">
<actions as="div">
<fill as="div" />
{task.isFinished && isSuccess && (
<>
<Button
type="button"
mod={['outlined']}
onClick={handleDelete}
>
Delete
</Button>
<Button
type="button"
tag='a'
href={getDownloadUrl()}
mod={['unelevated']}
onClick={handleDownload}
download
>
Download
</Button>
</>
)}
{task.isFinished && !isSuccess && (
<Button
type="button"
mod={['outlined']}
onClick={handleDetails}
disabled={idDetailsOpen}
>
{translate('ui_errors_details')}
</Button>
)}
{!task.isFinished && (
<Button
type="button"
mod={['outlined']}
onClick={handleCancel}
>
{translate('ui_processing_cancel')}
</Button>
)}
</actions>
</notification-body>
</notification>
);
});