mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #34 from dbeaver/feat-data-export
feat(data-export-plugin): some improvements
This commit is contained in:
@@ -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...',
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
});
|
||||
+110
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user