Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5be95ae
dbeaver/pro#8821 feat: add gql query for import
SychevAndrey Jul 29, 2026
29219db
Merge branch 'devel' into 8821-customer-wants-import-with-mergeupsert…
SychevAndrey Jul 29, 2026
6cadc0c
dbeaver/cloudbeaver#8821 throw readable exception on import fail
HocKu7 Jul 29, 2026
36df107
Merge remote-tracking branch 'origin/8821-customer-wants-import-with-…
HocKu7 Jul 29, 2026
226401a
dbeaver/pro#8821 feat: add import settings step
SychevAndrey Jul 29, 2026
ccbbbe0
dbeaver/pro#8821 refactor: gql types for driver config
SychevAndrey Jul 29, 2026
5b22323
dbeaver/pro#8821 refactor: use connectionKey
SychevAndrey Aug 5, 2026
eb186d8
Merge branch 'devel' into 8821-customer-wants-import-with-mergeupsert…
dariamarutkina Aug 5, 2026
74f5b2b
Merge branch 'devel' into 8821-customer-wants-import-with-mergeupsert…
dariamarutkina Aug 6, 2026
2b1d233
Merge branch 'devel' into 8821-customer-wants-import-with-mergeupsert…
dariamarutkina Aug 6, 2026
b32cfb0
Merge branch 'devel' into 8821-customer-wants-import-with-mergeupsert…
HocKu7 Aug 6, 2026
d0ee05a
dbeaver/pro#8821 add open New Connection
HocKu7 Aug 6, 2026
aef3f79
Merge remote-tracking branch 'origin/8821-customer-wants-import-with-…
HocKu7 Aug 6, 2026
d329bdd
dbeaver/pro#8821 feat: add openNewConnection checkbox and replace doc…
SychevAndrey Aug 7, 2026
ffdb862
dbeaver/pro#8821 feat: clear onDuplicateKey if useBulk is active
SychevAndrey Aug 7, 2026
2e3cd00
dbeaver/pro#8821 feat: add titles
SychevAndrey Aug 7, 2026
1a8f97c
Merge branch 'devel' into 8821-customer-wants-import-with-mergeupsert…
HocKu7 Aug 7, 2026
0be8145
dbeaver/pro#8821 refactor: remove extra checks (not a driver config, …
SychevAndrey Aug 7, 2026
cea5ca5
dbeaver/pro#8821 remove defaultOpenNewConnection from driverConfigura…
HocKu7 Aug 7, 2026
242c008
Merge remote-tracking branch 'origin/8821-customer-wants-import-with-…
HocKu7 Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ input DataTransferImportSettings {
useBulkLoad: Boolean
"Use transactions during import"
useTransactions: Boolean
"Open new connection(s) for the import task"
openNewConnection: Boolean
}

input DataTransferImportParameters {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ private void importData(
throw new DBWebException("Import is canceled");
}
} catch (DBException e) {
throw new DBWebException("Import failed cause: " + e.getMessage());
throw new DBWebException("Import failed", e);
}
}
}
Expand All @@ -446,6 +446,9 @@ private void applyImportSettings(
consumerSettings.setUseTransactions(CommonUtils.getBoolean(
settings.get(DTConstants.PROP_USE_TRANSACTIONS),
consumerSettings.isUseTransactions()));
consumerSettings.setOpenNewConnections(CommonUtils.getBoolean(
settings.get(DTConstants.PROP_OPEN_NEW_CONNECTION),
consumerSettings.isOpenNewConnections()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query getConnectionImportDriverConfiguration($projectId: ID, $connectionId: ID!) {
connections: userConnections(projectId: $projectId, id: $connectionId) {
id
Comment thread
sergeyteleshev marked this conversation as resolved.
projectId
driverConfiguration {
...DriverConfiguration
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fragment DriverConfiguration on DriverConfiguration {
supportedInsertReplaceMethods {
id
name
description
}
supportsBulkLoad
supportsTransactions
}
1 change: 1 addition & 0 deletions webapp/packages/plugin-data-import/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"@cloudbeaver/core-blocks": "workspace:*",
"@cloudbeaver/core-connections": "workspace:*",
"@cloudbeaver/core-di": "workspace:*",
"@cloudbeaver/core-dialogs": "workspace:*",
"@cloudbeaver/core-events": "workspace:*",
Expand Down
11 changes: 8 additions & 3 deletions webapp/packages/plugin-data-import/src/DataImportBootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createConnectionParam } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { ACTION_IMPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view';
Expand Down Expand Up @@ -32,7 +33,7 @@
super();
}

override register() {

Check warning on line 36 in webapp/packages/plugin-data-import/src/DataImportBootstrap.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Missing return type on function
this.actionService.addHandler({
id: 'data-import-base-handler',
contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX],
Expand Down Expand Up @@ -72,16 +73,20 @@
throw new Error('Execution context must be provided');
}

const { status, result: dialogResult } = await this.commonDialogService.open(DataImportDialogLazy, { tableName: model.name ?? model.id });
const connectionKey = createConnectionParam(executionContext.projectId, executionContext.connectionId);
const { status, result: dialogResult } = await this.commonDialogService.open(DataImportDialogLazy, {
tableName: model.name ?? model.id,
connectionKey,
});

if (status === DialogueStateResult.Resolved && dialogResult) {
const success = await this.dataImportService.importData(
executionContext.connectionId,
connectionKey,
executionContext.id,
executionContext.projectId,
result.id,
dialogResult.processorId,
dialogResult.file,
dialogResult.settings,
);

if (success) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 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-lite';

import { Button, CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, useTranslate } from '@cloudbeaver/core-blocks';
import { Button, CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, useResource, useTranslate } from '@cloudbeaver/core-blocks';
import type { IConnectionInfoParams } from '@cloudbeaver/core-connections';
import type { DialogComponent } from '@cloudbeaver/core-dialogs';
import type { DataTransferImportSettings } from '@cloudbeaver/core-sdk';

import { DataImportDriverConfigurationResource } from '../DataImportDriverConfigurationResource.js';
import { DataImportFileSelector } from './DataImportFileSelector.js';
import { EDataImportDialogStep } from './EDataImportDialogStep.js';
import type { IDataImportDialogState } from './IDataImportDialogState.js';
import { ImportProcessorList } from './ImportProcessorList.js';
import { ImportSettingsForm } from './ImportSettingsForm.js';
import { useDataImportDialog } from './useDataImportDialog.js';

export interface IDataImportDialogResult {
file: File;
processorId: string;
settings: DataTransferImportSettings;
}

export interface IDataImportDialogPayload {
tableName: string;
connectionKey: IConnectionInfoParams;
initialState?: IDataImportDialogState;
}

Expand All @@ -33,21 +39,51 @@ export const DataImportDialog: DialogComponent<IDataImportDialogPayload, IDataIm
}) {
const translate = useTranslate();
const dialog = useDataImportDialog(payload.initialState);
const driverConfigurationResource = useResource(
DataImportDialog,
DataImportDriverConfigurationResource,
payload.connectionKey,
{ silent: true },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why silent?

@SychevAndrey SychevAndrey Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that those setting are kinda optional, so if something goes wrong, we can just skip it. If we show an error or smth, we should think about an action the user can do. Retry to get the driver configuration or what? I would let them go and try import with default settings. And if there is a real problem, they will se an error there

@devnaumov devnaumov Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, but its better to default to no options for useResource and .data for getting data from resource

);

const driverConfiguration = driverConfigurationResource.tryGetData ?? null;
Comment thread
sergeyteleshev marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have specific behaviour here? why tryGetData instead of .data?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the answer above

const hasSettings =
!!driverConfiguration &&
(driverConfiguration.supportsBulkLoad ||
driverConfiguration.supportsTransactions ||
driverConfiguration.supportedInsertReplaceMethods.length > 0);

let title = translate('plugin_data_import_title');
let icon = '/icons/data-import.svg';

if (dialog.state.step === EDataImportDialogStep.File && dialog.state.selectedProcessor) {
if (dialog.state.step !== EDataImportDialogStep.Processor && dialog.state.selectedProcessor) {
title += ` (${dialog.state.selectedProcessor.name ?? dialog.state.selectedProcessor.id})`;
icon = dialog.state.selectedProcessor.icon ?? icon;
}

function importData() {
if (dialog.state.file && dialog.state.selectedProcessor) {
resolveDialog({ file: dialog.state.file, processorId: dialog.state.selectedProcessor.id, settings: dialog.state.settings });
}
Comment thread
sergeyteleshev marked this conversation as resolved.
}

function submitFileStep() {
if (driverConfiguration && hasSettings) {
dialog.goToSettings(driverConfiguration);
} else {
importData();
}
}

return (
<CommonDialogWrapper size="large" fixedSize>
<CommonDialogHeader title={title} subTitle={payload.tableName} icon={icon} onReject={rejectDialog} />
<CommonDialogBody noBodyPadding>
{dialog.state.step === EDataImportDialogStep.Processor && <ImportProcessorList onSelect={dialog.selectProcessor} />}
{dialog.state.step === EDataImportDialogStep.File && <DataImportFileSelector state={dialog.state} onDelete={dialog.deleteFile} />}
{dialog.state.step === EDataImportDialogStep.Settings && driverConfiguration && (
<ImportSettingsForm settings={dialog.state.settings} driverConfiguration={driverConfiguration} />
)}
</CommonDialogBody>

<CommonDialogFooter>
Expand All @@ -61,9 +97,20 @@ export const DataImportDialog: DialogComponent<IDataImportDialogPayload, IDataIm
</Button>
<Button
type="button"
loading={driverConfigurationResource.isLoading()}
disabled={!dialog.state.file || !dialog.state.selectedProcessor}
onClick={() => resolveDialog({ file: dialog.state.file!, processorId: dialog.state.selectedProcessor!.id })}
onClick={submitFileStep}
>
{translate(hasSettings ? 'ui_stepper_next' : 'ui_import')}
</Button>
</div>
)}
{dialog.state.step === EDataImportDialogStep.Settings && (
<div className="tw:flex tw:ml-auto tw:gap-2">
<Button type="button" variant="secondary" onClick={dialog.stepBack}>
{translate('ui_stepper_back')}
</Button>
<Button type="button" disabled={!dialog.state.file || !dialog.state.selectedProcessor} onClick={importData}>
{translate('ui_import')}
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 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 enum EDataImportDialogStep {
Processor,
File,
Settings,
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 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 type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';
import type { DataTransferImportSettings, DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';

import type { EDataImportDialogStep } from './EDataImportDialogStep.js';

export interface IDataImportDialogState {
step: EDataImportDialogStep;
file: File | null;
selectedProcessor: DataTransferProcessorInfo | null;
settings: DataTransferImportSettings;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 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-lite';
import { useEffect } from 'react';

import { Combobox, Container, FieldCheckbox, Link, useTranslate } from '@cloudbeaver/core-blocks';
import type { DataTransferImportSettings } from '@cloudbeaver/core-sdk';

import type { IDataImportDriverConfiguration } from '../DataImportDriverConfigurationResource.js';

interface Props {
settings: DataTransferImportSettings;
driverConfiguration: IDataImportDriverConfiguration;
}

export const ImportSettingsForm = observer<Props>(function ImportSettingsForm({ settings, driverConfiguration }) {
const translate = useTranslate();
const { supportedInsertReplaceMethods, supportsBulkLoad, supportsTransactions } = driverConfiguration;

useEffect(() => {
if (settings.useBulkLoad) {
settings.onDuplicateKeyMethod = undefined;
}
}, [settings.useBulkLoad]);

Check warning on line 29 in webapp/packages/plugin-data-import/src/DataImportDialog/ImportSettingsForm.tsx

View workflow job for this annotation

GitHub Actions / Frontend / Lint

React Hook useEffect has a missing dependency: 'settings'. Either include it or remove the dependency array

return (
<Container gap parent>
{supportedInsertReplaceMethods.length > 0 && (
<div>
<Combobox
name="onDuplicateKeyMethod"
state={settings}
items={supportedInsertReplaceMethods}
keySelector={method => method.id}
valueSelector={method => method.name}
titleSelector={method => method.description ?? undefined}
disabled={settings.useBulkLoad}
title={translate('plugin_data_import_settings_on_duplicate_key_title')}
placeholder={translate('plugin_data_import_settings_on_duplicate_key_placeholder')}
>
{translate('plugin_data_import_settings_on_duplicate_key')}
</Combobox>
<Link className="tw:text-xs" href="https://dbeaver.com/docs/cloudbeaver/Data-Import-and-Replace/" target="_blank">
{translate('plugin_data_import_settings_on_duplicate_key_help')}
</Link>
</div>
)}
{(supportsBulkLoad || supportsTransactions) && (
<Container vertical>
{supportsBulkLoad && (
<FieldCheckbox title={translate('plugin_data_import_settings_use_bulk_load_title')} name="useBulkLoad" state={settings}>
{translate('plugin_data_import_settings_use_bulk_load')}
</FieldCheckbox>
)}
{supportsTransactions && (
<FieldCheckbox title={translate('plugin_data_import_settings_use_transactions_title')} name="useTransactions" state={settings}>
{translate('plugin_data_import_settings_use_transactions')}
</FieldCheckbox>
)}
<FieldCheckbox title={translate('plugin_data_import_settings_open_new_connection_title')} name="openNewConnection" state={settings}>
{translate('plugin_data_import_settings_open_new_connection')}
</FieldCheckbox>
</Container>
)}
</Container>
);
});
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 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, observable } from 'mobx';

import { useObservableRef } from '@cloudbeaver/core-blocks';
import type { DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';
import type { DataTransferImportSettings, DataTransferProcessorInfo } from '@cloudbeaver/core-sdk';

import type { IDataImportDriverConfiguration } from '../DataImportDriverConfigurationResource.js';
import { EDataImportDialogStep } from './EDataImportDialogStep.js';
import type { IDataImportDialogState } from './IDataImportDialogState.js';

interface IDialog {
state: IDataImportDialogState;
stepBack: () => void;
goToSettings: (configuration: IDataImportDriverConfiguration) => void;
selectProcessor: (processor: DataTransferProcessorInfo) => void;
deleteFile: () => void;
reset: () => void;
Expand All @@ -25,17 +27,44 @@ const DEFAULT_STATE_GETTER: () => IDataImportDialogState = () => ({
step: EDataImportDialogStep.Processor,
file: null,
selectedProcessor: null,
settings: {},
});

export function useDataImportDialog(initialState?: IDataImportDialogState) {
const dialog = useObservableRef<IDialog>(
function getDefaultSettings(configuration: IDataImportDriverConfiguration): DataTransferImportSettings {
const settings: DataTransferImportSettings = {
openNewConnection: true,
};

if (configuration.supportsTransactions) {
settings.useTransactions = true;
}

if (configuration.supportedInsertReplaceMethods) {
settings.onDuplicateKeyMethod = undefined;
}

if (configuration.supportsBulkLoad) {
settings.useBulkLoad = false;
}

return settings;
}
Comment on lines +33 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to have 1 interface with settings so don't need to map it at all

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i got it why this happens:

because getting data and saving data has different interfaces on backend:
dataTransferImportDataIntoResults - saving
IDataImportDriverConfiguration- getting in resource


export function useDataImportDialog(initialState?: IDataImportDialogState): IDialog {
return useObservableRef<IDialog>(
() => ({
state: initialState ?? DEFAULT_STATE_GETTER(),
stepBack() {
if (this.state.step === EDataImportDialogStep.File) {
if (this.state.step === EDataImportDialogStep.Settings) {
this.state.step = EDataImportDialogStep.File;
} else if (this.state.step === EDataImportDialogStep.File) {
Comment thread
sergeyteleshev marked this conversation as resolved.
this.state.step = EDataImportDialogStep.Processor;
}
},
goToSettings(configuration: IDataImportDriverConfiguration) {
this.state.settings = { ...getDefaultSettings(configuration), ...this.state.settings };
this.state.step = EDataImportDialogStep.Settings;
},
selectProcessor(processor: DataTransferProcessorInfo) {
if (this.state.selectedProcessor && this.state.selectedProcessor.id !== processor.id) {
this.reset();
Expand All @@ -51,9 +80,14 @@ export function useDataImportDialog(initialState?: IDataImportDialogState) {
this.state = DEFAULT_STATE_GETTER();
},
}),
{ state: observable, stepBack: action.bound, selectProcessor: action.bound, deleteFile: action.bound, reset: action.bound },
{
state: observable,
stepBack: action.bound,
goToSettings: action.bound,
selectProcessor: action.bound,
deleteFile: action.bound,
reset: action.bound,
},
false,
);

return dialog;
}
Loading
Loading