mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(data-export-plugin): add export status notify
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user