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

fix(data-export-plugin): add menu item and tooltip
This commit is contained in:
Alexey
2020-05-18 18:17:27 +03:00
committed by GitHub
9 changed files with 119 additions and 38 deletions
@@ -56,7 +56,7 @@ export class ConnectionDialogsService {
id: 'closeConnection',
isPresent(context) {
return context.contextType === NavigationTreeContextMenuService.nodeContextType
&& Boolean((context.data as NodeWithParent)?.object?.features?.includes(EObjectFeature.dataSource));
&& !!context.data.object?.features?.includes(EObjectFeature.dataSource);
},
title: 'Disconnect',
onClick: (context: IMenuContext<NodeWithParent>) => {
@@ -17,4 +17,13 @@ export class NodeManagerUtils {
return connectionNodeId.replace('database://', '');
}
static nodeIdToConnectionId(nodeId: string): string {
const matches = nodeId.match(/database:\/\/(.*?)(|\/.*)$/);
if (!matches) {
throw new Error('Not database object');
}
return matches[1];
}
}
@@ -15,7 +15,7 @@ import { IMenuContext } from './IMenuContext';
export interface IContextMenuItem<T> extends IMenuItemOptions {
onClick?: (context: IMenuContext<T>) => void;
// if isPresent is false menu item will not be included in resulting context menu
isPresent: (context: IMenuContext<any>) => boolean;
isPresent: (context: IMenuContext<T>) => boolean;
isDisabled?: (context: IMenuContext<T>) => boolean;
// When the item is present in menu it can be hidden based on certain conditions
isHidden?: (context: IMenuContext<T>) => boolean;
@@ -6,8 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import {
NavigationTreeContextMenuService, NodeManagerUtils, NodeWithParent, EObjectFeature
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { IContextMenuItem, IMenuContext, CommonDialogService } from '@dbeaver/core/dialogs';
import {
IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService
} from '@dbeaver/core/dialogs';
import { TableFooterMenuService, TableViewerModel } from '@dbeaver/data-viewer-plugin';
import { DataExportDialog } from './Dialog/DataExportDialog';
@@ -18,6 +23,7 @@ export class DataExportMenuService {
constructor(
private commonDialogService: CommonDialogService,
private tableFooterMenuService: TableFooterMenuService,
private contextMenuService: ContextMenuService,
) { }
register() {
@@ -32,6 +38,27 @@ export class DataExportMenuService {
onClick: this.exportData.bind(this),
};
this.tableFooterMenuService.registerMenuItem(exportData);
this.contextMenuService.addMenuItem<NodeWithParent>(
this.contextMenuService.getRootMenuToken(),
{
id: 'export',
isPresent(context) {
return context.contextType === NavigationTreeContextMenuService.nodeContextType
&& !!context.data.object?.features?.includes(EObjectFeature.dataContainer);
},
order: 2,
title: 'Export',
onClick: (context) => {
const node = context.data;
const connectionId = NodeManagerUtils.nodeIdToConnectionId(node.id);
this.commonDialogService.open(DataExportDialog, {
connectionId,
containerNodePath: node.id,
});
},
}
);
}
private exportData(context: IMenuContext<TableViewerModel>) {
@@ -8,10 +8,12 @@
import { observable, computed } from 'mobx';
import { ErrorDetailsDialog } from '@dbeaver/core/app';
import { IProperty } from '@dbeaver/core/blocks';
import { injectable, IInitializableController } from '@dbeaver/core/di';
import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
import { DataTransferProcessorInfo, GQLErrorCatcher } from '@dbeaver/core/sdk';
import { DataExportService } from '../DataExportService';
import { IExportContext } from '../IExportContext';
@@ -22,7 +24,7 @@ export enum DataExportStep {
}
@injectable()
export class DataExportController implements IInitializableController {
export class DataExportController implements IInitializableController, IDestructibleController {
@observable step = DataExportStep.DataTransferProcessor
get isLoading() {
return this.dataExportService.processors.isLoading();
@@ -35,18 +37,22 @@ export class DataExportController implements IInitializableController {
.from(
this.dataExportService.processors.data.values()
)
.sort((a, b) => this.sortProcessors(a, b));
.sort(sortProcessors);
}
@observable processorProperties: any = {}
@observable properties: IProperty[] = []
readonly error = new GQLErrorCatcher();
private context!: IExportContext;
private close!: () => void;
private isDistructed = false;
constructor(
private dataExportService: DataExportService,
private notificationService: NotificationService
private notificationService: NotificationService,
private commonDialogService: CommonDialogService
) { }
init(context: IExportContext, close: () => void) {
@@ -55,6 +61,10 @@ export class DataExportController implements IInitializableController {
this.loadProcessors();
}
destruct(): void {
this.isDistructed = true;
}
prepareExport = async () => {
if (!this.processor || this.isExporting) {
return;
@@ -71,7 +81,9 @@ export class DataExportController implements IInitializableController {
);
this.close();
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t export');
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, 'Can\'t export');
}
} finally {
this.isExporting = false;
close();
@@ -100,6 +112,13 @@ export class DataExportController implements IInitializableController {
this.processorProperties = {};
this.step = DataExportStep.Configure;
this.error.clear();
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
private async loadProcessors() {
@@ -109,13 +128,13 @@ export class DataExportController implements IInitializableController {
this.notificationService.logException(exception, 'Can\'t load data export processors');
}
}
}
private sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number {
if (processorA.order === processorB.order)
{
return (processorA.name || '').localeCompare((processorB.name || ''));
}
function sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number {
if (processorA.order === processorB.order)
{
return (processorA.name || '').localeCompare((processorB.name || ''));
}
return processorA.order - processorB.order;
}
return processorA.order - processorB.order;
}
@@ -26,7 +26,9 @@ export const DataExportDialog: DialogComponent<IExportContext, null> = observer(
processor={controller.processor}
properties={controller.properties}
processorProperties={controller.processorProperties}
error={controller.error}
isExporting={controller.isExporting}
onShowDetails={controller.showDetails}
onBack={() => controller.setStep(DataExportStep.DataTransferProcessor)}
onClose={props.rejectDialog}
onExport={controller.prepareExport}
@@ -9,31 +9,49 @@
import { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { IProperty, PropertiesTable } from '@dbeaver/core/blocks';
import { IProperty, PropertiesTable, ErrorMessage } from '@dbeaver/core/blocks';
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
import { DataTransferProcessorInfo, GQLErrorCatcher } from '@dbeaver/core/sdk';
import { composes, useStyles } from '@dbeaver/core/theming';
import { ProcessorConfigureDialogFooter } from './ProcessorConfigureDialogFooter';
const styles = css`
CommonDialogWrapper {
max-height: 500px;
min-height: 500px;
}
PropertiesTable {
flex: 1;
}
message {
margin: auto;
}
`;
const styles = composes(
css`
Tab {
composes: theme-ripple theme-background-secondary theme-text-on-secondary from global;
}
ErrorMessage {
composes: theme-background-secondary from global;
}
`,
css`
CommonDialogWrapper {
max-height: 500px;
min-height: 500px;
}
PropertiesTable {
flex: 1;
}
message {
margin: auto;
}
ErrorMessage {
position: sticky;
bottom: 0;
padding: 8px 24px;
}
`
);
type ProcessorSelectDialogProps = {
processor: DataTransferProcessorInfo;
properties: IProperty[];
processorProperties: any;
error: GQLErrorCatcher;
isExporting: boolean;
onShowDetails(): void;
onClose(): void;
onBack(): void;
onExport(): void;
@@ -44,7 +62,9 @@ export const ProcessorConfigureDialog = observer(
processor,
properties,
processorProperties,
error,
isExporting,
onShowDetails,
onClose,
onBack,
onExport,
@@ -52,7 +72,7 @@ export const ProcessorConfigureDialog = observer(
const translate = useTranslate();
const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`;
return styled(styles)(
return styled(useStyles(styles))(
<CommonDialogWrapper
title={title}
noBodyPadding
@@ -66,11 +86,15 @@ export const ProcessorConfigureDialog = observer(
}
onReject={onClose}
>
{isExporting && <message as="div">{translate('data_transfer_dialog_preparation')}</message>}
{!isExporting && (
<PropertiesTable
properties={properties}
propertiesState={processorProperties}
<PropertiesTable
properties={properties}
propertiesState={processorProperties}
/>
{error.responseMessage && (
<ErrorMessage
text={error.responseMessage}
hasDetails={error.hasDetails}
onShowDetails={onShowDetails}
/>
)}
</CommonDialogWrapper>
@@ -66,7 +66,7 @@ export const ProcessorSelectDialog = observer(
>
<export-object as="div">
{context.sourceName ? translate('data_transfer_exporting_sql') : `${translate('data_transfer_exporting_table')} ${node?.name}`}
<pre>{context.sourceName}</pre>
<pre title={context.sourceName}>{context.sourceName}</pre>
</export-object>
{isLoading && <Loader />}
{!isLoading && <ExportProcessorList processors={processors} onSelect={onSelect}/>}
@@ -59,7 +59,7 @@ export const ExportNotification = observer(function ExportNotification({
<notification-body as="div">
<source-name as="div">
{controller.sourceName}
<pre>{controller.task?.context.sourceName}</pre>
<pre title={controller.task?.context.sourceName}>{controller.task?.context.sourceName}</pre>
</source-name>
<actions as="div">
{controller.isSuccess && (