-
Notifications
You must be signed in to change notification settings - Fork 552
dbeaver/pro#8821 feat: add import settings step #4506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Changes from all commits
5be95ae
29219db
6cadc0c
36df107
226401a
ccbbbe0
5b22323
eb186d8
74f5b2b
2b1d233
b32cfb0
d0ee05a
aef3f79
d329bdd
ffdb862
2e3cd00
1a8f97c
0be8145
cea5ca5
242c008
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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 | ||
| } |
| 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; | ||
| } | ||
|
|
||
|
|
@@ -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 }, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why silent?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
sergeyteleshev marked this conversation as resolved.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have specific behaviour here? why tryGetData instead of .data?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||
| } | ||
|
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> | ||
|
|
@@ -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> | ||
|
|
||
| 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]); | ||
|
|
||
| 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; | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
|
|
||
| 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) { | ||
|
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(); | ||
|
|
@@ -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; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.