Merge pull request #34 from dbeaver/feat-data-export

feat(data-export-plugin): some improvements
This commit is contained in:
Alexey
2020-05-07 18:03:01 +03:00
committed by GitHub
41 changed files with 527 additions and 438 deletions
+4 -1
View File
@@ -30,12 +30,15 @@ module.exports = {
data_transfer_dialog_title: 'Export data',
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',
data_transfer_notification_download: 'Download',
data_transfer_notification_delete: 'Delete',
data_transfer_exporting_table: 'Table:',
data_transfer_exporting_sql: 'SQL:',
ui_stepper_back: 'Back',
ui_processing_loading: 'Loading...',
+4 -1
View File
@@ -29,12 +29,15 @@ module.exports = {
data_transfer_dialog_title: 'Экспорт данных',
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: 'Подготовка файла к загрузке не удалась или была отменена',
data_transfer_notification_download: 'Загрузить',
data_transfer_notification_delete: 'Удалить',
data_transfer_exporting_table: 'Таблица:',
data_transfer_exporting_sql: 'SQL:',
ui_stepper_back: 'Назад',
ui_processing_loading: 'Загрузка...',
@@ -32,7 +32,7 @@ export const Notifications = observer(function Notifications() {
return styled(useStyles(styles))(
<Portal>
<notifications as="div">
{notificationService.notifications.map(notification => (
{notificationService.notificationList.values.map(notification => (
<NotificationsItem key={notification.id} notification={notification} />
))}
</notifications>
@@ -31,7 +31,7 @@ export class NotificationItemController implements IInitializableController {
this.handleShowDetails = this.showDetails.bind(this);
}
if (notification.type !== ENotificationType.Error && !notification.persistent) {
if (notification.type !== ENotificationType.Error) {
this.closeAfter = FadeTimeout;
}
}
@@ -19,7 +19,7 @@ type NotificationProps = {
}
export const NotificationsItem = observer(function Notification({ notification }: NotificationProps) {
if (!notification) {
if (notification.isSilent) {
return null;
}
const controller = useController(NotificationItemController, notification);
@@ -7,10 +7,9 @@
*/
import { useEffect, useState } from 'react';
import { Button as ReakitButton } from 'reakit/Button';
import styled, { use } from 'reshadow';
import { Icon, Button } from '@dbeaver/core/blocks';
import { Button, IconButton } from '@dbeaver/core/blocks';
import { ENotificationType } from '@dbeaver/core/eventsLog';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
@@ -66,9 +65,7 @@ export function Snackbar({
{type && <NotificationMark type={type} />}
<message as="div">{text}</message>
{!closeAfter && onClose && (
<ReakitButton onClick={onClose}>
<Icon name="cross" viewBox="0 0 16 16" />
</ReakitButton>
<IconButton onClick={onClose} name="cross" viewBox="0 0 16 16" />
)}
</notification-header>
<notification-body as="div">
@@ -47,22 +47,6 @@ export const SNACKBAR_STYLES = composes(
display: flex;
}
ReakitButton {
color: rgba(0, 0, 0, 0.45);
outline: none;
border: none;
background: none;
cursor: pointer;
padding: 0;
margin: 4px;
height: 16px;
& Icon {
width: 16px;
height: 16px;
}
}
message {
flex: 1;
overflow: auto;
@@ -98,11 +82,18 @@ export const SNACKBAR_STYLES = composes(
}
actions {
margin-top: 24px;
display: flex;
&:not(:empty) {
margin-top: 24px;
}
& Button {
margin-left: auto;
display: block;
margin-left: 16px;
&:first-child {
margin-left: auto;
}
}
}
`
@@ -10,8 +10,6 @@ export interface ITabOptions<T = any> {
id?: string;
handlerId: string;
handlerState: T;
name?: string;
icon?: string;
}
export interface ITab<T = any> extends ITabOptions<T> {
@@ -47,8 +47,10 @@ export class NavigationTabsService {
return this.state.tabs;
}
onTabSelect = new Subject<ITab>();
onTabClose = new Subject<ITab>();
private tabSelectSubject = new Subject<ITab>();
private tabCloseSubject = new Subject<ITab>();
readonly onTabSelect = this.tabSelectSubject.asObservable();
readonly onTabClose = this.tabCloseSubject.asObservable();
constructor(
private notificationService: NotificationService,
@@ -64,8 +66,6 @@ export class NavigationTabsService {
if (
typeof value.id === 'string'
&& typeof value.handlerId === 'string'
&& (!value.name || typeof value.name === 'string')
&& (!value.icon || typeof value.icon === 'string')
) {
map[key] = value;
}
@@ -102,7 +102,7 @@ export class NavigationTabsService {
this.callHandlerCallback(tab, handler => handler.onSelect);
}
this.onTabSelect.next(tab);
this.tabSelectSubject.next(tab);
}
@action async closeTab(tabId: string, skipHandlers?: boolean) {
@@ -111,7 +111,7 @@ export class NavigationTabsService {
await this.callHandlerCallback(tab, handler => handler.onClose);
}
this.onTabClose.next(tab);
this.tabCloseSubject.next(tab);
this.state.history = this.state.history.filter(id => id !== tabId);
this.tabsMap.delete(tabId);
this.state.tabs = this.state.tabs.filter(id => id !== tabId);
@@ -31,8 +31,6 @@ export type TabRestoreEvent<T = any> = (tab: ITab<T>) => Promise<boolean> | bool
export type TabHandlerOptions<TState = any> = {
key: string;
navigatorId: string;
priority: number;
getTabComponent(): TabHandlerTabComponent<TState>;
getPanelComponent(): TabHandlerPanelComponent<TState>;
onSelect?: TabHandlerEvent<TState>;
@@ -43,7 +41,6 @@ export type TabHandlerOptions<TState = any> = {
export class TabHandler<TState = any> {
key: string
navigatorId: string
getTabComponent: () => TabHandlerTabComponent<TState>;
getPanelComponent: () => TabHandlerPanelComponent<TState>;
onSelect?: TabHandlerEvent<TState>
@@ -53,7 +50,6 @@ export class TabHandler<TState = any> {
constructor(options: TabHandlerOptions<TState>) {
this.key = options.key;
this.navigatorId = options.navigatorId;
this.getTabComponent = options.getTabComponent;
this.getPanelComponent = options.getPanelComponent;
this.onSelect = options.onSelect;
@@ -0,0 +1,35 @@
import { Button, ButtonProps } from 'reakit/Button';
import styled, { css } from 'reshadow';
import { Icon } from './Icons/Icon';
const styles = css`
Button {
color: rgba(0, 0, 0, 0.45);
outline: none;
border: none;
background: none;
cursor: pointer;
padding: 0;
margin: 4px;
height: 16px;
& Icon {
width: 16px;
height: 16px;
}
}
`;
type Props = {
name: string;
viewBox: string;
}
export function IconButton({ name, viewBox, ...rest }: Props & ButtonProps) {
return styled(styles)(
<Button {...rest}>
<Icon name={name} viewBox={viewBox} />
</Button>
);
}
+1
View File
@@ -17,3 +17,4 @@ export * from './SubmittingForm';
export * from './SanitizedHTML/SanitizedHTML';
export * from './ItemList';
export * from './PropertiesTable';
export * from './IconButton';
@@ -9,6 +9,7 @@
export enum ENotificationType {
Info = 'Info',
Error = 'Error',
Custom =' Custom'
}
export type NotificationComponentProps<T = undefined> = {
@@ -23,7 +24,6 @@ export interface INotification<T = undefined> {
title: string;
message?: string;
details?: string | Error;
persistent: boolean;
isSilent: boolean;
customComponent?: () => NotificationComponent<T>;
source: T;
@@ -6,27 +6,19 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { GQLError, ServerInternalError } from '@dbeaver/core/sdk';
import { EntityList } from '@dbeaver/core/utils';
import { OrderedMap } from '@dbeaver/core/utils';
import { ENotificationType, INotification, INotificationOptions } from './INotification';
import {
ENotificationType, INotification, INotificationOptions, NotificationComponent
} from './INotification';
@injectable()
export class NotificationService {
private notificationList = new EntityList<number, INotification<any>>(({ id }) => id);
readonly notificationList = new OrderedMap<number, INotification<any>>(({ id }) => id);
private notificationNextId = 0
/**
* By default filtered without silent notifications
*/
@computed get notifications(): INotification<any>[] {
return this.notificationList.values
.filter(notification => !notification.isSilent);
}
notify<T = never>(options: INotificationOptions<T>, type: ENotificationType) {
const id = this.notificationNextId++;
@@ -36,14 +28,26 @@ export class NotificationService {
message: options.message,
details: options.details,
isSilent: !!options.isSilent,
persistent: !!options.persistent,
customComponent: options.customComponent,
source: options.source!,
type,
close: this.close.bind(this, id),
showDetails: this.showDetails.bind(this, id),
};
this.notificationList.set(notification);
this.notificationList.addValue(notification);
}
customNotification<T = never>(
component: () => NotificationComponent<T>,
source?: T,
options?: INotificationOptions<T>
) {
this.notify({
title: '',
...options,
customComponent: component,
source,
}, ENotificationType.Custom);
}
logInfo<T>(notification: INotificationOptions<T>) {
@@ -5,12 +5,14 @@ mutation asyncExportTaskStatus(
id: $taskId
) {
id
name
running
taskResult
status
error {
message
errorCode
stackTrace
}
taskResult
}
}
@@ -13,7 +13,7 @@ import {
import { SessionSettingsService } from './SessionSettingsService';
export type SessionState = Pick<SessionInfo, 'id' | 'createTime' | 'cacheExpired' | 'lastAccessTime' | 'locale'> & {
export type SessionState = Pick<SessionInfo, 'createTime' | 'cacheExpired' | 'lastAccessTime' | 'locale'> & {
connections: Array<Pick<ConnectionInfo, 'id' | 'name' | 'connected' | 'driverId'>>;
};
@@ -1,6 +1,5 @@
mutation openSession {
session: openSession {
id
createTime
lastAccessTime
cacheExpired
@@ -1,6 +1,5 @@
query sessionState {
sessionState {
id
createTime
lastAccessTime
locale
+11 -7
View File
@@ -472,6 +472,8 @@ export type QueryMetadataGetNodeDdlArgs = {
export type ServerConfig = {
name: Scalars["String"];
version: Scalars["String"];
anonymousAccessEnabled?: Maybe<Scalars["Boolean"]>;
authenticationEnabled?: Maybe<Scalars["Boolean"]>;
supportsPredefinedConnections?: Maybe<Scalars["Boolean"]>;
supportsProvidedConnections?: Maybe<Scalars["Boolean"]>;
supportsCustomConnections?: Maybe<Scalars["Boolean"]>;
@@ -501,7 +503,6 @@ export type ServerMessage = {
};
export type SessionInfo = {
id: Scalars["ID"];
createTime: Scalars["String"];
lastAccessTime: Scalars["String"];
locale: Scalars["String"];
@@ -859,7 +860,10 @@ export type AsyncExportTaskStatusMutationVariables = {
};
export type AsyncExportTaskStatusMutation = {
taskInfo: Pick<AsyncTaskInfo, "id" | "running" | "taskResult"> & {
taskInfo: Pick<
AsyncTaskInfo,
"id" | "name" | "running" | "status" | "taskResult"
> & {
error: Maybe<Pick<ServerError, "message" | "errorCode" | "stackTrace">>;
};
};
@@ -1251,7 +1255,7 @@ export type OpenSessionMutation = {
session: Maybe<
Pick<
SessionInfo,
"id" | "createTime" | "lastAccessTime" | "cacheExpired" | "locale"
"createTime" | "lastAccessTime" | "cacheExpired" | "locale"
> & {
connections: Array<
Maybe<Pick<ConnectionInfo, "id" | "name" | "driverId" | "connected">>
@@ -1288,7 +1292,7 @@ export type SessionStateQuery = {
sessionState: Maybe<
Pick<
SessionInfo,
"id" | "createTime" | "lastAccessTime" | "locale" | "cacheExpired"
"createTime" | "lastAccessTime" | "locale" | "cacheExpired"
> & {
connections: Array<
Maybe<Pick<ConnectionInfo, "id" | "name" | "driverId" | "connected">>
@@ -1507,13 +1511,15 @@ export const AsyncExportTaskStatusDocument = gql`
mutation asyncExportTaskStatus($taskId: String!) {
taskInfo: asyncTaskStatus(id: $taskId) {
id
name
running
taskResult
status
error {
message
errorCode
stackTrace
}
taskResult
}
}
`;
@@ -1890,7 +1896,6 @@ export const UpdateResultsDataDocument = gql`
export const OpenSessionDocument = gql`
mutation openSession {
session: openSession {
id
createTime
lastAccessTime
cacheExpired
@@ -1927,7 +1932,6 @@ export const ServerConfigDocument = gql`
export const SessionStateDocument = gql`
query sessionState {
sessionState {
id
createTime
lastAccessTime
locale
@@ -1,54 +0,0 @@
/*
* 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 { action, computed, observable } from 'mobx';
export class EntityList<K, V> {
private identifiers = observable.array<K>([]);
@observable private map: Map<K, V> = new Map<K, V>();
@computed get keys(): K[] {
return this.identifiers;
}
@computed get values(): V[] {
return this.identifiers.map(i => this.map.get(i)!);
}
constructor(private keySelector: (val: V) => K) { }
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
@action
set(value: V): void {
const key = this.keySelector(value);
this.identifiers.push(key);
this.map.set(key, value);
}
@action
remove(key: K): void {
if (!this.map.has(key)) {
return;
}
this.map.delete(key);
this.identifiers.remove(key);
}
@action
clear(): void {
this.identifiers.clear();
this.map.clear();
}
}
@@ -74,9 +74,10 @@ export class Deferred<T> {
}
@action
protected toCancelled(): void {
protected toCancelled(reason?: any): void {
this.state = EDeferredState.CANCELLED;
this.promiseExecutor.reject(new PromiseCancelledError());
this.rejectionReason = reason;
this.promiseExecutor.reject(new PromiseCancelledError(reason));
}
@action
@@ -101,7 +102,7 @@ export class DeferredFromPromise<T> extends Deferred<T> {
value => this.toResolved(value),
(err) => {
if (err instanceof PromiseCancelledError) {
this.toCancelled();
this.toCancelled(err.reason);
} else {
this.toRejected(err);
}
@@ -8,8 +8,8 @@
export class PromiseCancelledError extends Error {
constructor(message?: string) {
super(message);
constructor(public reason?: Error) {
super(reason?.message);
}
}
-1
View File
@@ -1,7 +1,6 @@
export * from './OrderedMap';
export * from './uuid';
export * from './parseJSONFlat';
export * from './EntityList';
export * from './Promises/CancellablePromise';
export * from './Promises/Deferred';
@@ -30,7 +30,6 @@ export class DataExportMenuService {
title: 'Export',
icon: 'export',
onClick: this.exportData.bind(this),
// isPanel: true,
};
this.tableFooterMenuService.registerMenuItem(exportData);
}
@@ -0,0 +1,129 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GraphQLService, DataTransferParameters } from '@dbeaver/core/sdk';
import { Deferred, OrderedMap } from '@dbeaver/core/utils';
import { ExportFromContainerProcess } from './ExportFromContainerProcess';
import { ExportFromResultsProcess } from './ExportFromResultsProcess';
import { IExportContext } from './IExportContext';
type Process = {
taskId: string;
process: Deferred<string>;
}
type ExportProcess = {
taskId: string;
context: IExportContext;
parameters: DataTransferParameters;
process: Deferred<string>;
}
@injectable()
export class DataExportProcessService {
readonly exportProcesses = new OrderedMap<string, ExportProcess>(value => value.taskId);
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService,
) { }
async cancel(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
process.process.cancel();
}
async delete(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
try {
const dataFileId = process.process.getPayload();
if (dataFileId) {
await this.graphQLService.gql.removeDataTransferFile({ dataFileId });
}
} catch (exception) {
this.notificationService.logException(exception, 'Error occurred while deleting file');
} finally {
this.exportProcesses.remove(exportId);
}
}
download(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.process.getPayload();
if (!dataFileId) {
return;
}
this.exportProcesses.remove(exportId);
}
downloadUrl(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.process.getPayload();
if (!dataFileId) {
return;
}
return `/dbeaver/data/${dataFileId}`;
}
async exportData(
context: IExportContext,
parameters: DataTransferParameters
): Promise<string> {
let process: Process;
if (context.containerNodePath) {
process = await this.exportFromContainer(context.connectionId, context.containerNodePath, parameters);
} else {
process = await this.exportFromResults(context.connectionId, context.contextId!, context.resultId!, parameters);
}
this.exportProcesses.addValue({
...process,
context,
parameters,
});
return process.taskId;
}
private async exportFromContainer(
connectionId: string,
containerNodePath: string,
parameters: DataTransferParameters
): Promise<Process> {
const process = new ExportFromContainerProcess(this.graphQLService, this.notificationService);
const taskId = await process.start(connectionId, containerNodePath, parameters);
return { taskId, process };
}
private async exportFromResults(
connectionId: string,
contextId: string,
resultsId: string,
parameters: DataTransferParameters
): Promise<Process> {
const process = new ExportFromResultsProcess(this.graphQLService, this.notificationService);
const taskId = await process.start(connectionId, contextId, resultsId, parameters);
return { taskId, process };
}
}
@@ -6,18 +6,15 @@
* 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
} from '@dbeaver/core/sdk';
import { Deferred } from '@dbeaver/core/utils';
import { ExportFromContainerProcess } from './ExportFromContainerProcess';
import { ExportFromResultsProcess } from './ExportFromResultsProcess';
import { PendingNotification } from './PendingNotification';
import { DataExportProcessService } from './DataExportProcessService';
import { ExportNotification } from './ExportNotification/ExportNotification';
import { IExportContext } from './IExportContext';
type ProcessorsResourceMetadata = {
loaded: boolean;
@@ -30,109 +27,40 @@ export class DataExportService {
this.refreshProcessorsAsync.bind(this),
(_, { loaded }) => loaded
);
readonly exportProcesses = new Map<string, Deferred<string>>();
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private commonDialogService: CommonDialogService,
private dataExportProcessService: DataExportProcessService,
) { }
async cancel(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
process.cancel();
await this.dataExportProcessService.cancel(exportId);
}
async delete(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.getPayload();
if (!dataFileId) {
return;
}
try {
await this.graphQLService.gql.removeDataTransferFile({ dataFileId });
this.exportProcesses.delete(exportId);
} 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 {
}
await this.dataExportProcessService.delete(exportId);
}
download(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.getPayload();
if (!dataFileId) {
return;
}
this.exportProcesses.delete(exportId);
this.dataExportProcessService.download(exportId);
}
downloadUrl(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.getPayload();
if (!dataFileId) {
return;
}
return `/dbeaver/data/${dataFileId}`;
return this.dataExportProcessService.download(exportId);
}
async exportFromContainer(
connectionId: string,
containerNodePath: string,
async exportData(
context: IExportContext,
parameters: DataTransferParameters
): Promise<string | undefined> {
const process = new ExportFromContainerProcess(this.graphQLService, this.notificationService);
const taskId = await process.start(connectionId, containerNodePath, parameters);
if (taskId) {
this.exportProcesses.set(taskId, process);
this.showPendingNotification(taskId);
return taskId;
}
}
) {
const taskId = await this.dataExportProcessService.exportData(
context,
parameters
);
async exportFromResults(
connectionId: string,
contextId: string,
resultsId: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
const process = new ExportFromResultsProcess(this.graphQLService, this.notificationService);
const taskId = await process.start(connectionId, contextId, resultsId, parameters);
if (taskId) {
this.exportProcesses.set(taskId, process);
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,
});
this.notificationService.customNotification(() => ExportNotification, taskId);
return taskId;
}
private async refreshProcessorsAsync(
@@ -55,33 +55,20 @@ export class DataExportController implements IInitializableController {
this.loadProcessors();
}
export = async () => {
prepareExport = async () => {
if (!this.processor || this.isExporting) {
return;
}
this.isExporting = true;
try {
if (this.context.containerNodePath) {
await this.dataExportService.exportFromContainer(
this.context.connectionId,
this.context.containerNodePath,
{
processorId: this.processor.id,
processorProperties: this.processorProperties,
}
);
} else if (this.context.contextId && this.context.resultId) {
await this.dataExportService.exportFromResults(
this.context.connectionId,
this.context.contextId,
this.context.resultId,
{
processorId: this.processor.id,
processorProperties: this.processorProperties,
}
);
}
await this.dataExportService.exportData(
this.context,
{
processorId: this.processor.id,
processorProperties: this.processorProperties,
}
);
this.close();
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t export');
@@ -128,6 +115,6 @@ export class DataExportController implements IInitializableController {
return (processorA.name || '').localeCompare((processorB.name || ''));
}
return (processorB.order || 0) - (processorA.order || 0);
return processorA.order - processorB.order;
}
}
@@ -29,7 +29,7 @@ export const DataExportDialog: DialogComponent<IExportContext, null> = observer(
isExporting={controller.isExporting}
onBack={() => controller.setStep(DataExportStep.DataTransferProcessor)}
onClose={props.rejectDialog}
onExport={controller.export}
onExport={controller.prepareExport}
/>
);
}
@@ -29,7 +29,14 @@ const styles = css`
}
export-object {
composes: theme-typography--body2 from global;
flex-shrink: 0;
padding: 16px;
max-height: 50px;
overflow: hidden;
& pre {
margin: 0;
}
}
`;
@@ -58,7 +65,10 @@ export const ProcessorSelectDialog = observer(
noBodyPadding
onReject={onClose}
>
<export-object as="div">{translate('data_transfer_dialog_exporting_object')} {context.sourceName || node?.name}</export-object>
<export-object as="div">
{context.sourceName ? translate('data_transfer_exporting_sql') : `${translate('data_transfer_exporting_table')} ${node?.name}`}
<pre>{context.sourceName}</pre>
</export-object>
{isLoading && <Loader />}
{!isLoading && <ExportProcessorList processors={processors} onSelect={onSelect}/>}
</CommonDialogWrapper>
@@ -32,7 +32,7 @@ export class ExportFromContainerProcess extends Deferred<string> {
connectionId: string,
containerNodePath: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
): Promise<string> {
// start async task
try {
const { taskInfo } = await this.graphQLService.gql.exportDataFromContainer({
@@ -45,14 +45,14 @@ export class ExportFromContainerProcess extends Deferred<string> {
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
this.statusUpdateProcess();
return this.taskId;
} catch (e) {
this.onError(e);
throw e;
}
this.statusUpdateProcess();
return this.taskId;
}
/**
@@ -133,9 +133,7 @@ export class ExportFromContainerProcess extends Deferred<string> {
private onError(error: Error, status?: string) {
// if task failed to execute during cancelling - it means it was cancelled successfully
if (this.getState() === EDeferredState.CANCELLING) {
this.toCancelled();
const message = `Data export has been canceled${status ? `: ${status}` : ''}`;
this.notificationService.logException(error, message);
this.toCancelled(error);
} else {
this.toRejected(error);
}
@@ -33,7 +33,7 @@ export class ExportFromResultsProcess extends Deferred<string> {
contextId: string,
resultsId: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
): Promise<string> {
// start async task
try {
const { taskInfo } = await this.graphQLService.gql.exportDataFromResults({
@@ -47,14 +47,14 @@ export class ExportFromResultsProcess extends Deferred<string> {
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
this.statusUpdateProcess();
return this.taskId;
} catch (e) {
this.onError(e);
throw e;
}
this.statusUpdateProcess();
return this.taskId;
}
/**
@@ -135,9 +135,7 @@ export class ExportFromResultsProcess extends Deferred<string> {
private onError(error: Error, status?: string) {
// if task failed to execute during cancelling - it means it was cancelled successfully
if (this.getState() === EDeferredState.CANCELLING) {
this.toCancelled();
const message = `Data export has been canceled${status ? `: ${status}` : ''}`;
this.notificationService.logException(error, message);
this.toCancelled(error);
} else {
this.toRejected(error);
}
@@ -0,0 +1,110 @@
/*
* 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 } from 'react';
import styled, { use, css } from 'reshadow';
import { SNACKBAR_STYLES } from '@dbeaver/core/app';
import { Button, Loader, IconButton } from '@dbeaver/core/blocks';
import { useController } 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 { ExportNotificationController } from './ExportNotificationController';
const styles = css`
Loader {
margin-right: 16px;
}
source-name {
composes: theme-typography--body2 from global;
padding-top: 16px;
max-height: 50px;
overflow: hidden;
& pre {
margin: 0;
}
}
`;
export const ExportNotification = observer(function ExportNotification({
notification,
}: NotificationComponentProps<string>) {
const controller = useController(ExportNotificationController, notification);
const translate = useTranslate();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return styled(useStyles(SNACKBAR_STYLES, styles))(
<notification as="div" {...use({ mounted })} >
<notification-header as="div">
<Loader loading={controller.isPending} hideMessage />
<message as="div">{translate(controller.status)}</message>
{!controller.isPending && (
<IconButton onClick={controller.delete} name="cross" viewBox="0 0 16 16" />
)}
</notification-header>
<notification-body as="div">
<source-name as="div">
{controller.sourceName}
<pre>{controller.task?.context.sourceName}</pre>
</source-name>
<actions as="div">
{controller.isSuccess && (
<>
<Button
type="button"
mod={['outlined']}
onClick={controller.delete}
>
{translate('data_transfer_notification_delete')}
</Button>
<Button
type="button"
tag='a'
href={controller.downloadUrl}
mod={['unelevated']}
onClick={controller.download}
download
>
{translate('data_transfer_notification_download')}
</Button>
</>
)}
{controller.hasDetails && (
<Button
type="button"
mod={['outlined']}
onClick={controller.showDetails}
disabled={controller.isDetailsDialogOpen}
>
{translate('ui_errors_details')}
</Button>
)}
{controller.isPending && (
<Button
type="button"
mod={['outlined']}
onClick={controller.cancel}
disabled={controller.process?.getState() === EDeferredState.CANCELLING}
>
{translate('ui_processing_cancel')}
</Button>
)}
</actions>
</notification-body>
</notification>
);
});
@@ -0,0 +1,110 @@
/*
* 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 { observable, computed } from 'mobx';
import { ErrorDetailsDialog, NodesManagerService } from '@dbeaver/core/app';
import { IInitializableController, injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { INotification } from '@dbeaver/core/eventsLog';
import { LocalizationService } from '@dbeaver/core/localization';
import { EDeferredState } from '@dbeaver/core/utils';
import { DataExportProcessService } from '../DataExportProcessService';
@injectable()
export class ExportNotificationController implements IInitializableController {
@observable isDetailsDialogOpen = false;
get isSuccess() {
return this.process?.getState() === EDeferredState.RESOLVED;
}
get isPending() {
return this.process?.isInProgress;
}
get process() {
return this.task?.process;
}
get task() {
return this.dataExportProcessService.exportProcesses.get(this.notification.source);
}
get hasDetails() {
return !!this.process?.getRejectionReason();
}
@computed get sourceName() {
if (!this.task) {
return '';
}
if (this.task.context.containerNodePath) {
const node = this.nodesManagerService.getNode(this.task.context.containerNodePath);
return `${this.localization.translate('data_transfer_exporting_table')} ${node?.name}`;
}
return this.localization.translate('data_transfer_exporting_sql');
}
get status() {
switch (this.process?.getState()) {
case EDeferredState.PENDING:
return 'data_transfer_notification_preparation';
case EDeferredState.CANCELLING:
return 'ui_processing_canceling';
case EDeferredState.RESOLVED:
return 'data_transfer_notification_ready';
default:
return 'data_transfer_notification_error';
}
}
get downloadUrl() {
return this.dataExportProcessService.downloadUrl(this.notification.source);
}
private notification!: INotification<any>;
constructor(
private commonDialogService: CommonDialogService,
private dataExportProcessService: DataExportProcessService,
private nodesManagerService: NodesManagerService,
private localization: LocalizationService,
) {}
init(notification: INotification<any>) {
this.notification = notification;
}
delete = () => {
this.dataExportProcessService.delete(this.notification.source);
this.notification.close();
}
download = () => {
this.dataExportProcessService.download(this.notification.source);
this.notification.close();
}
cancel = () => {
this.dataExportProcessService.cancel(this.notification.source);
}
showDetails= async () => {
this.isDetailsDialogOpen = true;
try {
this.notification.showDetails();
await this.commonDialogService.open(ErrorDetailsDialog, this.process?.getRejectionReason());
} finally {
this.isDetailsDialogOpen = false;
}
}
}
@@ -1,163 +0,0 @@
/*
* 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 && task.getRejectionReason() && (
<Button
type="button"
mod={['outlined']}
onClick={handleDetails}
disabled={idDetailsOpen}
>
{translate('ui_errors_details')}
</Button>
)}
{!task.isFinished && (
<Button
type="button"
mod={['outlined']}
onClick={handleCancel}
disabled={task.getState() === EDeferredState.CANCELLING}
>
{translate('ui_processing_cancel')}
</Button>
)}
</actions>
</notification-body>
</notification>
);
});
@@ -10,16 +10,18 @@ import { IServiceInjector, PluginManifest } from '@dbeaver/core/di';
import { Bootstrap } from './Bootstrap';
import { DataExportMenuService } from './DataExportMenuService';
import { DataExportProcessService } from './DataExportProcessService';
import { DataExportService } from './DataExportService';
export const manifest: PluginManifest = {
info: {
name: 'Data Viewer Plugin',
name: 'Data Export Plugin',
},
providers: [
DataExportMenuService,
DataExportService,
DataExportProcessService,
],
async initialize(services: IServiceInjector) {
@@ -33,7 +33,7 @@ export class TableFooterMenuService {
return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems;
}
registerMenuItem(options: IContextMenuItem<TableViewerModel>, panelId?: string): void {
this.contextMenuService.addMenuItem<TableViewerModel>(panelId || this.tableFooterMenuToken, options);
registerMenuItem(options: IContextMenuItem<TableViewerModel>): void {
this.contextMenuService.addMenuItem<TableViewerModel>(this.tableFooterMenuToken, options);
}
}
@@ -34,7 +34,7 @@ export interface ITableViewerModelOptions {
containerNodePath?: string;
resultId?: string | null; // will be filled after fist data fetch
executionContext?: IExecutionContext | null; // will be filled before fist data fetch
sourceName?: string;
sourceName?: string; // TODO: refactor it, used for showing sql query for export
initialState?: IRequestDataResult;
noLoaderWhileRequestingDataAsync?: boolean;
requestDataAsync(model: TableViewerModel, rowOffset: number, count: number): Promise<IRequestDataResult>;
@@ -9,6 +9,8 @@
export interface IObjectViewerTabState {
objectId: string;
folderId: string;
tabIcon?: string;
tabTitle?: string;
pageId: string;
pagesState: Map<string, any>;
}
@@ -22,8 +22,8 @@ export const ObjectViewerTab = observer(function ObjectViewerTab({
return styled(useStyles(...style))(
<Tab tabId={tab.id} onOpen={onSelect} onClose={onClose} >
<TabIcon icon={node?.icon || tab.icon} />
<TabTitle title={node?.name || tab.name} />
<TabIcon icon={node?.icon || tab.handlerState.tabIcon} />
<TabTitle title={node?.name || tab.handlerState.tabTitle} />
</Tab>
);
});
@@ -46,8 +46,6 @@ export class ObjectViewerTabService {
this.tabHandler = this.navigationTabsService
.registerTabHandler<IObjectViewerTabState>({
key: objectViewerTabHandlerKey,
navigatorId: 'database',
priority: 1,
getTabComponent: () => ObjectViewerTab,
getPanelComponent: () => ObjectViewerPanel,
onRestore: this.restoreObjectTab.bind(this),
@@ -132,11 +130,16 @@ export class ObjectViewerTabService {
}
async restoreObjectTab(tab: ITab<IObjectViewerTabState>) {
if (typeof tab.handlerState?.folderId === 'string' && typeof tab.handlerState?.objectId === 'string') {
if (
typeof tab.handlerState?.folderId === 'string'
&& typeof tab.handlerState?.objectId === 'string'
&& (!tab.handlerState.tabIcon || typeof tab.handlerState.tabIcon === 'string')
&& (!tab.handlerState.tabTitle || typeof tab.handlerState.tabTitle === 'string')
) {
const node = await this.nodesManagerService.loadNodeInfo(tab.handlerState.objectId);
if (node) {
tab.icon = node.icon;
tab.name = node.name;
tab.handlerState.tabIcon = node.icon;
tab.handlerState.tabTitle = node.name;
return this.dbObjectPageService.restorePages(tab);
}
@@ -162,8 +165,8 @@ export class ObjectViewerTabService {
);
if (tab) {
tab.name = nodeInfo.name;
tab.icon = nodeInfo.icon;
tab.handlerState.tabIcon = nodeInfo.icon;
tab.handlerState.tabTitle = nodeInfo.name;
tabInfo.registerTab(tab);
} else {
tabInfo.openNewTab<IObjectViewerTabState>({
@@ -173,9 +176,9 @@ export class ObjectViewerTabService {
folderId: nodeInfo.folderId,
pageId: '',
pagesState: new Map(),
tabIcon: nodeInfo.icon,
tabTitle: nodeInfo.name,
},
name: nodeInfo.name,
icon: nodeInfo.icon,
});
}
}
@@ -43,8 +43,6 @@ export class SqlEditorTabService {
this.tabHandler = this.navigationTabsService
.registerTabHandler<ISqlEditorTabState>({
key: sqlEditorTabHandlerKey,
navigatorId: 'sql-editor',
priority: 1,
getTabComponent: () => SqlEditorTab,
getPanelComponent: () => SqlEditorPanel,
onRestore: this.handleTabRestore.bind(this),