chore: remastered build system

This commit is contained in:
Wroud
2020-06-19 19:31:20 +03:00
parent 9fd0a8c01f
commit ff1928b2b8
749 changed files with 6294 additions and 6096 deletions
@@ -0,0 +1,17 @@
# dependencies
/node_modules
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,37 @@
{
"name": "@cloudbeaver/plugin-data-export",
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/plugin-data-viewer": "^0.1.0",
"@cloudbeaver/core-app": "^0.1.0",
"@cloudbeaver/core-blocks": "^0.1.0",
"@cloudbeaver/core-di": "^0.1.0",
"@cloudbeaver/core-dialogs": "^0.1.0",
"@cloudbeaver/core-events": "^0.1.0",
"@cloudbeaver/core-extensions": "^0.1.0",
"@cloudbeaver/core-localization": "^0.1.0",
"@cloudbeaver/core-plugin": "^0.1.0",
"@cloudbeaver/core-product": "^0.1.0",
"@cloudbeaver/core-root": "^0.1.0",
"@cloudbeaver/core-sdk": "^0.1.0",
"@cloudbeaver/core-settings": "^0.1.0",
"@cloudbeaver/core-theming": "^0.1.0",
"@cloudbeaver/core-utils": "^0.1.0",
"react": "^16.x.x",
"rxjs": "^6.x.x",
"mobx": "^5.x.x",
"mobx-react": "^6.x.x",
"reshadow": "^0.0.1-alpha.78"
}
}
@@ -0,0 +1,20 @@
/*
* 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 '@cloudbeaver/core-di';
import { DataExportMenuService } from './DataExportMenuService';
@injectable()
export class Bootstrap {
constructor(private dataExportMenuService: DataExportMenuService) { }
bootstrap() {
this.dataExportMenuService.register();
}
}
@@ -0,0 +1,72 @@
/*
* 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 {
NavigationTreeContextMenuService, NodeManagerUtils, NavNode, EObjectFeature
} from '@cloudbeaver/core-app';
import { injectable } from '@cloudbeaver/core-di';
import {
IContextMenuItem, IMenuContext, CommonDialogService, ContextMenuService
} from '@cloudbeaver/core-dialogs';
import { TableFooterMenuService, TableViewerModel } from '@cloudbeaver/plugin-data-viewer';
import { DataExportDialog } from './Dialog/DataExportDialog';
@injectable()
export class DataExportMenuService {
constructor(
private commonDialogService: CommonDialogService,
private tableFooterMenuService: TableFooterMenuService,
private contextMenuService: ContextMenuService,
) { }
register() {
const exportData: IContextMenuItem<TableViewerModel> = {
id: 'export ',
isPresent(context) {
return context.contextType === TableFooterMenuService.nodeContextType;
},
order: 1,
title: 'Export',
icon: 'export',
onClick: this.exportData.bind(this),
};
this.tableFooterMenuService.registerMenuItem(exportData);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.getRootMenuToken(),
{
id: 'export',
isPresent(context) {
return context.contextType === NavigationTreeContextMenuService.nodeContextType
&& context.data.objectFeatures.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>) {
this.commonDialogService.open(DataExportDialog, {
connectionId: context.data.connectionId,
contextId: context.data.executionContext?.contextId,
containerNodePath: context.data.containerNodePath,
resultId: context.data.resultId,
sourceName: context.data.sourceName,
});
}
}
@@ -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 '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GraphQLService, DataTransferParameters } from '@cloudbeaver/core-sdk';
import { Deferred, OrderedMap } from '@cloudbeaver/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 };
}
}
@@ -0,0 +1,80 @@
/*
* 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 '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import {
CachedResource, GraphQLService, DataTransferProcessorInfo, DataTransferParameters
} from '@cloudbeaver/core-sdk';
import { DataExportProcessService } from './DataExportProcessService';
import { ExportNotification } from './ExportNotification/ExportNotification';
import { IExportContext } from './IExportContext';
type ProcessorsResourceMetadata = {
loaded: boolean;
}
@injectable()
export class DataExportService {
readonly processors = new CachedResource(
new Map(),
this.refreshProcessorsAsync.bind(this),
(_, { loaded }) => loaded
);
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService,
private dataExportProcessService: DataExportProcessService,
) { }
async cancel(exportId: string) {
await this.dataExportProcessService.cancel(exportId);
}
async delete(exportId: string) {
await this.dataExportProcessService.delete(exportId);
}
download(exportId: string) {
this.dataExportProcessService.download(exportId);
}
downloadUrl(exportId: string) {
return this.dataExportProcessService.download(exportId);
}
async exportData(
context: IExportContext,
parameters: DataTransferParameters
) {
const taskId = await this.dataExportProcessService.exportData(
context,
parameters
);
this.notificationService.customNotification(() => ExportNotification, taskId);
return taskId;
}
private async refreshProcessorsAsync(
data: Map<string, DataTransferProcessorInfo>,
metadata: ProcessorsResourceMetadata,
): Promise<Map<string, DataTransferProcessorInfo>> {
const { processors } = await this.graphQLService.gql.getDataTransferProcessors();
data.clear();
for (const processor of processors) {
data.set(processor.id, processor);
}
metadata.loaded = true;
return data;
}
}
@@ -0,0 +1,140 @@
/*
* 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 } from '@cloudbeaver/core-app';
import { IProperty } from '@cloudbeaver/core-blocks';
import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { DataTransferProcessorInfo, GQLErrorCatcher } from '@cloudbeaver/core-sdk';
import { DataExportService } from '../DataExportService';
import { IExportContext } from '../IExportContext';
export enum DataExportStep {
DataTransferProcessor,
Configure
}
@injectable()
export class DataExportController implements IInitializableController, IDestructibleController {
@observable step = DataExportStep.DataTransferProcessor
get isLoading() {
return this.dataExportService.processors.isLoading();
}
@observable isExporting = false
@observable processor: DataTransferProcessorInfo | null = null
@computed get processors(): DataTransferProcessorInfo[] {
return Array
.from(
this.dataExportService.processors.data.values()
)
.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 commonDialogService: CommonDialogService
) { }
init(context: IExportContext, close: () => void) {
this.context = context;
this.close = close;
this.loadProcessors();
}
destruct(): void {
this.isDistructed = true;
}
prepareExport = async () => {
if (!this.processor || this.isExporting) {
return;
}
this.isExporting = true;
try {
await this.dataExportService.exportData(
this.context,
{
processorId: this.processor.id,
processorProperties: this.processorProperties,
}
);
this.close();
} catch (exception) {
if (!this.error.catch(exception) || this.isDistructed) {
this.notificationService.logException(exception, 'Can\'t export');
}
} finally {
this.isExporting = false;
close();
}
}
setStep = (step: DataExportStep) => {
this.step = step;
}
selectProcessor = (processorId: string) => {
this.processor = this.dataExportService
.processors
.data
.get(processorId)!;
this.properties = this.processor.properties?.map(property => ({
id: property.id!,
key: property.id!,
displayName: property.displayName!,
description: property.description,
validValues: property.validValues,
defaultValue: property.defaultValue,
})) || [];
this.processorProperties = {};
this.step = DataExportStep.Configure;
this.error.clear();
}
showDetails = () => {
if (this.error.exception) {
this.commonDialogService.open(ErrorDetailsDialog, this.error.exception);
}
}
private async loadProcessors() {
try {
await this.dataExportService.processors.load();
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load data export processors');
}
}
}
function sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number {
if (processorA.order === processorB.order)
{
return (processorA.name || '').localeCompare((processorB.name || ''));
}
return processorA.order - processorB.order;
}
@@ -0,0 +1,50 @@
/*
* 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 { useController } from '@cloudbeaver/core-di';
import { DialogComponentProps } from '@cloudbeaver/core-dialogs';
import { IExportContext } from '../IExportContext';
import { DataExportController, DataExportStep } from './DataExportController';
import { ProcessorConfigureDialog } from './ProcessorConfigureDialog';
import { ProcessorSelectDialog } from './ProcessorSelectDialog';
export const DataExportDialog = observer(function DataExportDialog({
payload,
rejectDialog,
}: DialogComponentProps<IExportContext, null>) {
const controller = useController(DataExportController, payload, rejectDialog);
if (controller.step === DataExportStep.Configure && controller.processor) {
return (
<ProcessorConfigureDialog
processor={controller.processor}
properties={controller.properties}
processorProperties={controller.processorProperties}
error={controller.error}
isExporting={controller.isExporting}
onShowDetails={controller.showDetails}
onBack={() => controller.setStep(DataExportStep.DataTransferProcessor)}
onClose={rejectDialog}
onExport={controller.prepareExport}
/>
);
}
return (
<ProcessorSelectDialog
context={payload}
processors={controller.processors}
onSelect={controller.selectProcessor}
isLoading={controller.isLoading}
onClose={rejectDialog}
/>
);
});
@@ -0,0 +1,33 @@
/*
* 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 { ItemList } from '@cloudbeaver/core-blocks';
import { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';
import { ProcessorItem } from './ProcessorItem';
type ExportProcessorListProps = {
processors: DataTransferProcessorInfo[];
onSelect(processorId: string): void;
className?: string;
}
export const ExportProcessorList = observer(function ExportProcessorList({
processors,
onSelect,
className,
}: ExportProcessorListProps) {
return (
<ItemList className={className}>
{processors.map(processor => <ProcessorItem key={processor.id} processor={processor} onSelect={onSelect}/>)}
</ItemList>
);
});
@@ -0,0 +1,26 @@
/*
* 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 { useCallback } from 'react';
import { ListItem } from '@cloudbeaver/core-blocks';
import { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';
type ProcessorItemProps = {
processor: DataTransferProcessorInfo;
onSelect(processorId: string): void;
}
export const ProcessorItem = observer(function ProcessorItem({
processor,
onSelect,
}: ProcessorItemProps) {
const select = useCallback(() => onSelect(processor.id), [processor]);
return <ListItem icon={processor.icon} name={processor.name} description={processor.description} onClick={select}/>;
});
@@ -0,0 +1,103 @@
/*
* 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 styled, { css } from 'reshadow';
import { IProperty, PropertiesTable, ErrorMessage } from '@cloudbeaver/core-blocks';
import { CommonDialogWrapper } from '@cloudbeaver/core-dialogs';
import { useTranslate } from '@cloudbeaver/core-localization';
import { DataTransferProcessorInfo, GQLErrorCatcher } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ProcessorConfigureDialogFooter } from './ProcessorConfigureDialogFooter';
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;
}
export const ProcessorConfigureDialog = observer(
function ProcessorConfigureDialog({
processor,
properties,
processorProperties,
error,
isExporting,
onShowDetails,
onClose,
onBack,
onExport,
}: ProcessorSelectDialogProps) {
const translate = useTranslate();
const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`;
return styled(useStyles(styles))(
<CommonDialogWrapper
title={title}
noBodyPadding
footer={
<ProcessorConfigureDialogFooter
isExporting={isExporting}
onExport={onExport}
onBack={onBack}
onCancel={onClose}
/>
}
onReject={onClose}
>
<PropertiesTable
properties={properties}
propertiesState={processorProperties}
/>
{error.responseMessage && (
<ErrorMessage
text={error.responseMessage}
hasDetails={error.hasDetails}
onShowDetails={onShowDetails}
/>
)}
</CommonDialogWrapper>
);
}
);
@@ -0,0 +1,79 @@
/*
* 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 styled, { css } from 'reshadow';
import { Button } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
const styles = css`
controls {
display: flex;
flex: 1;
height: 100%;
align-items: center;
margin: auto;
}
fill {
flex: 1;
}
Button:not(:first-child) {
margin-left: 24px;
}
`;
type Props = {
isExporting: boolean;
onCancel(): void;
onExport(): void;
onBack(): void;
}
export const ProcessorConfigureDialogFooter = observer(
function ProcessorConfigureDialogFooter({
isExporting,
onCancel,
onExport,
onBack,
}: Props) {
const translate = useTranslate();
return styled(styles)(
<controls as="div">
<Button
type="button"
mod={['outlined']}
onClick={onBack}
disabled={isExporting}
>
{translate('ui_stepper_back')}
</Button>
<fill as="div"/>
<Button
type="button"
mod={['outlined']}
onClick={onCancel}
disabled={isExporting}
>
{translate('ui_processing_cancel')}
</Button>
<Button
type="button"
mod={['unelevated']}
onClick={onExport}
disabled={isExporting}
>
{translate('data_transfer_dialog_export')}
</Button>
</controls>
);
}
);
@@ -0,0 +1,76 @@
/*
* 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 styled, { css } from 'reshadow';
import { useNode } from '@cloudbeaver/core-app';
import { Loader } from '@cloudbeaver/core-blocks';
import { CommonDialogWrapper } from '@cloudbeaver/core-dialogs';
import { useTranslate } from '@cloudbeaver/core-localization';
import { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';
import { IExportContext } from '../IExportContext';
import { ExportProcessorList } from './ExportProcessorList/ExportProcessorList';
const styles = css`
CommonDialogWrapper {
max-height: 500px;
min-height: 500px;
}
ExportProcessorList {
flex: 1;
}
export-object {
composes: theme-typography--body2 from global;
flex-shrink: 0;
padding: 16px;
max-height: 50px;
overflow: hidden;
& pre {
margin: 0;
}
}
`;
type ProcessorSelectDialogProps = {
context: IExportContext;
processors: DataTransferProcessorInfo[];
isLoading: boolean;
onSelect(processorId: string): void;
onClose(): void;
}
export const ProcessorSelectDialog = observer(
function ProcessorSelectDialog({
context,
processors,
isLoading,
onSelect,
onClose,
}: ProcessorSelectDialogProps) {
const translate = useTranslate();
const { node } = useNode(context.containerNodePath || '');
return styled(styles)(
<CommonDialogWrapper
title={translate('data_transfer_dialog_title')}
noBodyPadding
onReject={onClose}
>
<export-object as="div">
{context.sourceName ? translate('data_transfer_exporting_sql') : `${translate('data_transfer_exporting_table')} ${node?.name}`}
<pre title={context.sourceName}>{context.sourceName}</pre>
</export-object>
{isLoading && <Loader />}
{!isLoading && <ExportProcessorList processors={processors} onSelect={onSelect}/>}
</CommonDialogWrapper>
);
}
);
@@ -0,0 +1,141 @@
/*
* 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 { NotificationService } from '@cloudbeaver/core-events';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, DataTransferParameters,
} from '@cloudbeaver/core-sdk';
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState,
} from '@cloudbeaver/core-utils';
const DELAY_BETWEEN_TRIES = 1000;
// TODO: seems we need special abstraction over async tasks to manage it
export class ExportFromContainerProcess extends Deferred<string> {
private taskId?: string;
private timeout?: CancellablePromise<void>;
private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync
constructor(private graphQLService: GraphQLService,
private notificationService: NotificationService) {
super();
}
async start(
connectionId: string,
containerNodePath: string,
parameters: DataTransferParameters
): Promise<string> {
// start async task
try {
const { taskInfo } = await this.graphQLService.gql.exportDataFromContainer({
connectionId,
containerNodePath,
parameters,
});
this.applyResult(taskInfo);
this.taskId = taskInfo.id;
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
this.statusUpdateProcess();
return this.taskId;
} catch (e) {
this.onError(e);
throw e;
}
}
/**
* this method just mark process as cancelling
* to avoid racing conditions the server request will be executed in synchronous manner in start method
*/
cancel() {
if (this.getState() !== EDeferredState.PENDING) {
return;
}
this.toCancelling();
if (this.timeout) {
this.timeout.cancel();
}
}
private async statusUpdateProcess() {
if (this.isFinished || !this.taskId) {
return;
}
// check async task status until execution finished
while (this.isInProgress) {
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
// run the first check immediately because usually the query execution is fast
try {
const { taskInfo } = await this.graphQLService.gql.asyncExportTaskStatus({ taskId: this.taskId });
this.applyResult(taskInfo);
if (this.isFinished) {
return;
}
} catch (e) {
this.notificationService.logException(e, 'Failed to check async task status');
}
try {
this.timeout = cancellableTimeout(DELAY_BETWEEN_TRIES);
await this.timeout;
} catch { }
}
}
private async cancelAsync(taskId: string) {
if (this.isCancelConfirmed) {
return;
}
try {
await this.graphQLService.gql.asyncTaskCancel({ taskId });
this.isCancelConfirmed = true;
} catch (e) {
if (this.getState() === EDeferredState.CANCELLING) {
this.toPending();
this.notificationService.logException(e, 'Failed to cancel async task');
}
}
}
private applyResult(taskInfo: AsyncTaskInfo): void {
// task is running
if (taskInfo.running) {
return;
}
// task failed to execute
if (taskInfo.error) {
const serverError = new ServerInternalError(taskInfo.error);
this.onError(serverError, taskInfo.status);
return;
}
if (!taskInfo.taskResult) {
this.onError(new Error('Tasks execution returns no taskResult'), taskInfo.status);
return;
}
// task execution successful
this.toResolved(taskInfo.taskResult);
}
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(error);
} else {
this.toRejected(error);
}
}
}
@@ -0,0 +1,143 @@
/*
* 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 { NotificationService } from '@cloudbeaver/core-events';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, DataTransferParameters,
} from '@cloudbeaver/core-sdk';
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState,
} from '@cloudbeaver/core-utils';
const DELAY_BETWEEN_TRIES = 1000;
// TODO: seems we need special abstraction over async tasks to manage it
export class ExportFromResultsProcess extends Deferred<string> {
private taskId?: string;
private timeout?: CancellablePromise<void>;
private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync
constructor(private graphQLService: GraphQLService,
private notificationService: NotificationService) {
super();
}
async start(
connectionId: string,
contextId: string,
resultsId: string,
parameters: DataTransferParameters
): Promise<string> {
// start async task
try {
const { taskInfo } = await this.graphQLService.gql.exportDataFromResults({
connectionId,
contextId,
resultsId,
parameters,
});
this.applyResult(taskInfo);
this.taskId = taskInfo.id;
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
this.statusUpdateProcess();
return this.taskId;
} catch (e) {
this.onError(e);
throw e;
}
}
/**
* this method just mark process as cancelling
* to avoid racing conditions the server request will be executed in synchronous manner in start method
*/
cancel() {
if (this.getState() !== EDeferredState.PENDING) {
return;
}
this.toCancelling();
if (this.timeout) {
this.timeout.cancel();
}
}
private async statusUpdateProcess() {
if (this.isFinished || !this.taskId) {
return;
}
// check async task status until execution finished
while (this.isInProgress) {
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
// run the first check immediately because usually the query execution is fast
try {
const { taskInfo } = await this.graphQLService.gql.asyncExportTaskStatus({ taskId: this.taskId });
this.applyResult(taskInfo);
if (this.isFinished) {
return;
}
} catch (e) {
this.notificationService.logException(e, 'Failed to check async task status');
}
try {
this.timeout = cancellableTimeout(DELAY_BETWEEN_TRIES);
await this.timeout;
} catch { }
}
}
private async cancelAsync(taskId: string) {
if (this.isCancelConfirmed) {
return;
}
try {
await this.graphQLService.gql.asyncTaskCancel({ taskId });
this.isCancelConfirmed = true;
} catch (e) {
if (this.getState() === EDeferredState.CANCELLING) {
this.toPending();
this.notificationService.logException(e, 'Failed to cancel async task');
}
}
}
private applyResult(taskInfo: AsyncTaskInfo): void {
// task is running
if (taskInfo.running) {
return;
}
// task failed to execute
if (taskInfo.error) {
const serverError = new ServerInternalError(taskInfo.error);
this.onError(serverError, taskInfo.status);
return;
}
if (!taskInfo.taskResult) {
this.onError(new Error('Tasks execution returns no taskResult'), taskInfo.status);
return;
}
// task execution successful
this.toResolved(taskInfo.taskResult);
}
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(error);
} else {
this.toRejected(error);
}
}
}
@@ -0,0 +1,113 @@
/*
* 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 '@cloudbeaver/core-app';
import { Button, Loader, IconButton } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { NotificationComponentProps } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { EDeferredState } from '@cloudbeaver/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;
}
}
IconButton {
color: rgba(0, 0, 0, 0.45);
}
`;
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 title={controller.task?.context.sourceName}>{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, NavNodeManagerService } from '@cloudbeaver/core-app';
import { IInitializableController, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { INotification } from '@cloudbeaver/core-events';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { EDeferredState } from '@cloudbeaver/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.navNodeManagerService.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 navNodeManagerService: NavNodeManagerService,
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;
}
}
}
@@ -0,0 +1,15 @@
/*
* 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.
*/
export interface IExportContext {
connectionId: string;
contextId?: string;
containerNodePath?: string;
resultId?: string | null;
sourceName?: string;
}
@@ -0,0 +1,6 @@
import { manifest } from './manifest';
export default manifest;
export * from './DataExportMenuService';
export * from './DataExportService';
@@ -0,0 +1,32 @@
/*
* 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 { IServiceInjector, PluginManifest } from '@cloudbeaver/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 Export Plugin',
},
providers: [
DataExportMenuService,
DataExportService,
DataExportProcessService,
],
async initialize(services: IServiceInjector) {
services
.resolveServiceByClass(Bootstrap)
.bootstrap();
},
};
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"exclude": [
"node_modules"
],
"include": ["src/**/*"]
}