Skip to content
81 changes: 81 additions & 0 deletions webapp/packages/core-blocks/src/Export/ExportButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 { observable } from 'mobx';
import { observer } from 'mobx-react-lite';

import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import type { TLocalizationToken } from '@cloudbeaver/core-localization';

import { Icon } from '../Icon.js';
import { useTranslate } from '../localization/useTranslate.js';
import { useObservableRef } from '../useObservableRef.js';
import { ExportConfirmationDialog, type IExportConfirmationDialogState } from './ExportConfirmationDialog.js';
import { GridAction } from './GridAction.js';
import type { IExportFilterEntry } from './IExportFilterEntry.js';

export interface IExportButtonProps {
filters?: IExportFilterEntry[];
title?: TLocalizationToken;
confirmTitle?: TLocalizationToken;
descriptionWithFilters?: TLocalizationToken;
descriptionDefault?: TLocalizationToken;
/** Hint shown in the info box when `showHint` is enabled */
hint?: TLocalizationToken;
openOptionsLabel?: TLocalizationToken;
showHint?: boolean;
exportHandler: () => void;
onOpenOptions?: () => void;
}

export const ExportButton = observer<IExportButtonProps>(function ExportButton({

@sergeyteleshev sergeyteleshev Aug 7, 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.

Moved this component to core-blocks cause previously it was in plugin-query-manager and I don't wanna import this plugin into plugin-user-export - this is incorrect deps handling

filters,
title = 'core_blocks_export_title',
confirmTitle = 'core_blocks_export_confirm_title',
descriptionWithFilters = 'core_blocks_export_confirm_with_filters',
descriptionDefault = 'core_blocks_export_confirm_default',
hint = 'core_blocks_export_confirm_default',
openOptionsLabel = 'core_blocks_export_change_filters',
showHint,
exportHandler,
onOpenOptions,
}) {
const translate = useTranslate();
const commonDialogService = useService(CommonDialogService);

const dialogState = useObservableRef<IExportConfirmationDialogState>(
() => ({ filters: filters ?? [] }),
{ filters: observable.ref },
{ filters: filters ?? [] },
);

async function handleExport() {
const { status } = await commonDialogService.open(ExportConfirmationDialog, {
state: dialogState,
confirmTitle,
descriptionWithFilters,
descriptionDefault,
hint,
openOptionsLabel,
showHint,
onOpenOptions,
});

if (status !== DialogueStateResult.Resolved) {
return;
}

exportHandler();
}

return (
<GridAction title={translate(title)} onClick={handleExport}>
<Icon className="tw:w-full tw:h-full tw:fill-(--theme-primary)" name="table-export" viewBox="0 0 24 24" />
</GridAction>
);
});
12 changes: 12 additions & 0 deletions webapp/packages/core-blocks/src/Export/ExportButtonLazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { importLazyComponent } from '../importLazyComponent.js';

export const ExportButton = importLazyComponent(() => import('./ExportButton.js').then(m => m.ExportButton));

export type { IExportButtonProps } from './ExportButton.js';
104 changes: 104 additions & 0 deletions webapp/packages/core-blocks/src/Export/ExportConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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 type { DialogComponentProps } from '@cloudbeaver/core-dialogs';
import type { TLocalizationToken } from '@cloudbeaver/core-localization';

import { Button } from '../Button.js';
import { CommonDialogBody } from '../CommonDialog/CommonDialog/CommonDialogBody.js';
import { CommonDialogFooter } from '../CommonDialog/CommonDialog/CommonDialogFooter.js';
import { CommonDialogHeader } from '../CommonDialog/CommonDialog/CommonDialogHeader.js';
import { CommonDialogWrapper } from '../CommonDialog/CommonDialog/CommonDialogWrapper.js';
import { Container } from '../Containers/Container.js';
import { Fill } from '../Fill.js';
import { IconOrImage } from '../IconOrImage.js';
import { Translate } from '../localization/Translate.js';
import { useTranslate } from '../localization/useTranslate.js';
import { Tag } from '../Tags/Tag.js';
import { Tags } from '../Tags/Tags.js';
import type { IExportFilterEntry } from './IExportFilterEntry.js';

export interface IExportConfirmationDialogState {
filters: IExportFilterEntry[];
}

export interface IExportConfirmationDialogPayload {
state: IExportConfirmationDialogState;
confirmTitle: TLocalizationToken;
descriptionWithFilters: TLocalizationToken;
descriptionDefault: TLocalizationToken;
hint: TLocalizationToken;
openOptionsLabel: TLocalizationToken;
showHint?: boolean;
onOpenOptions?: () => void;
}

export const ExportConfirmationDialog = observer<DialogComponentProps<IExportConfirmationDialogPayload>>(function ExportConfirmationDialog({
payload,
resolveDialog,
rejectDialog,
className,
}) {
const translate = useTranslate();
const { state, confirmTitle, descriptionWithFilters, descriptionDefault, hint, openOptionsLabel, showHint, onOpenOptions } = payload;
const hasFilters = state.filters.length > 0;

return (
<CommonDialogWrapper size="medium" className={className} fixedWidth>
<CommonDialogHeader title={confirmTitle} onReject={() => rejectDialog()} />
<CommonDialogBody>
<>
<div className="tw:mb-3">
<Translate token={hasFilters ? descriptionWithFilters : descriptionDefault} />
</div>
{hasFilters && (
<ul className="tw:flex tw:flex-col tw:gap-1 tw:mb-3">
{state.filters.map(f => (
<li key={f.key} className="tw:flex tw:items-start tw:gap-1">
<strong className="tw:shrink-0">{translate(f.label)}:</strong>{' '}
{f.items ? (
<Tags className="tw:inline-flex tw:align-middle">
{f.items.map(item => (
<Tag key={String(item.id)} id={item.id} label={item.label} icon={item.icon} />
))}
</Tags>
) : (
f.value
)}
</li>
))}
</ul>
)}
{hasFilters && showHint && (
<div className="tw:flex tw:items-start tw:gap-2 tw:bg-(--theme-secondary) tw:p-3 tw:rounded tw:mt-auto">
<IconOrImage className="tw:py-1 tw:px-0.5" icon="/icons/preload/info_icon_sm.svg" />
<span className="tw:text-pretty">{translate(hint)}</span>
</div>
)}
</>
</CommonDialogBody>
<CommonDialogFooter>
{onOpenOptions && (
<Container keepSize>
<Button variant="secondary" onClick={onOpenOptions}>
{translate(openOptionsLabel)}
</Button>
</Container>
)}
<Fill />
<Container keepSize noWrap gap>
<Button variant="secondary" onClick={() => rejectDialog()}>
{translate('ui_processing_cancel')}
</Button>
<Button onClick={() => resolveDialog()}>{translate('ui_export')}</Button>
</Container>
</CommonDialogFooter>
</CommonDialogWrapper>
);
});
18 changes: 18 additions & 0 deletions webapp/packages/core-blocks/src/Export/GridAction.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.
*/

.gridAction {
composes: theme-ripple from global;
position: relative;
box-sizing: border-box;
background: inherit;
cursor: pointer;
width: 28px;
height: 100%;
padding: 4px;
}
23 changes: 23 additions & 0 deletions webapp/packages/core-blocks/src/Export/GridAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { s } from '../s.js';
import { useS } from '../useS.js';
import styles from './GridAction.module.css';

type Props = React.PropsWithChildren & React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;

export const GridAction = observer(function GridAction({ children, className, ...rest }: Props) {
const style = useS(styles);
return (
<button className={s(style, { gridAction: true }, className)} {...rest}>
{children}
</button>
);
});
17 changes: 17 additions & 0 deletions webapp/packages/core-blocks/src/Export/IExportFilterEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 type { TLocalizationToken } from '@cloudbeaver/core-localization';

import type { ITag } from '../Tags/Tag.js';

export interface IExportFilterEntry<T = string> {
key: T;
label: TLocalizationToken;
value?: string;
items?: ITag[];
}
4 changes: 4 additions & 0 deletions webapp/packages/core-blocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,23 @@

export * from './AuthenticationProviderLoader.js';
export * from './useAuthenticationAction.js';
export * from './CommonDialog/CommonDialog/CommonDialogBody.js';

Check failure on line 16 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './CommonDialog/CommonDialog/CommonDialogFooter.js';

Check failure on line 17 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './CommonDialog/CommonDialog/CommonDialogHeader.js';

Check failure on line 18 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './CommonDialog/CommonDialog/CommonDialogWrapper.js';

Check failure on line 19 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './CommonDialog/ConfirmationDialog.js';

Check failure on line 20 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export { default as ConfirmationDialogStyles } from './CommonDialog/ConfirmationDialog.module.css';
export * from './CommonDialog/ConfirmationDialogDelete.js';

Check failure on line 22 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './CommonDialog/RenameDialog.js';

Check failure on line 23 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './CommonDialog/DialogsPortal.js';

Check failure on line 24 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './ExportImageDialog/ExportImageDialogLazy.js';
export * from './ExportImageDialog/ExportImageFormats.js';

export * from './Export/ExportButtonLazy.js';
export * from './Export/GridAction.js';

Check failure on line 29 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()
export * from './Export/IExportFilterEntry.js';

export * from './ErrorDetailsDialog/ErrorDetailsDialog.js';

Check failure on line 32 in webapp/packages/core-blocks/src/index.ts

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Don't import/export .tsx files from .ts files directly, use React.lazy()

export * from './ComponentsRegistry/CRegistryLoader.js';
export * from './ComponentsRegistry/registry.js';
Expand Down
5 changes: 5 additions & 0 deletions webapp/packages/core-blocks/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ export default [
['core_blocks_export_image_dialog_title', 'Export as Image'],
['core_blocks_export_image_dialog_format', 'File Format'],
['core_blocks_export_image_dialog_transparent_background', 'Transparent background'],
['core_blocks_export_title', 'Export to CSV'],
['core_blocks_export_confirm_title', 'Export'],
['core_blocks_export_confirm_with_filters', 'The export will include data with the following filters:'],
['core_blocks_export_confirm_default', 'All data will be exported.'],
['core_blocks_export_change_filters', 'Change Filters'],
['core_blocks_dialog_element_close_tooltip', 'Close panel'],
];
5 changes: 5 additions & 0 deletions webapp/packages/core-blocks/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ export default [
['core_blocks_export_image_dialog_title', "Exporter en tant qu'image"],
['core_blocks_export_image_dialog_format', 'Format de fichier'],
['core_blocks_export_image_dialog_transparent_background', 'Fond transparent'],
['core_blocks_export_title', 'Export to CSV'],
['core_blocks_export_confirm_title', 'Export'],
['core_blocks_export_confirm_with_filters', 'The export will include data with the following filters:'],
['core_blocks_export_confirm_default', 'All data will be exported.'],
['core_blocks_export_change_filters', 'Change Filters'],
['core_blocks_dialog_element_close_tooltip', 'Fermer le panneau'],
];
5 changes: 5 additions & 0 deletions webapp/packages/core-blocks/src/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ export default [
['core_blocks_export_image_dialog_title', 'Esporta come immagine'],
['core_blocks_export_image_dialog_format', 'Formato file'],
['core_blocks_export_image_dialog_transparent_background', 'Sfondo trasparente'],
['core_blocks_export_title', 'Export to CSV'],
['core_blocks_export_confirm_title', 'Export'],
['core_blocks_export_confirm_with_filters', 'The export will include data with the following filters:'],
['core_blocks_export_confirm_default', 'All data will be exported.'],
['core_blocks_export_change_filters', 'Change Filters'],
['core_blocks_dialog_element_close_tooltip', 'Chiudi pannello'],
];
5 changes: 5 additions & 0 deletions webapp/packages/core-blocks/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ export default [
['core_blocks_export_image_dialog_title', 'Экспортировать как изображение'],
['core_blocks_export_image_dialog_format', 'Формат файла'],
['core_blocks_export_image_dialog_transparent_background', 'Прозрачный фон'],
['core_blocks_export_title', 'Экспорт в CSV'],
['core_blocks_export_confirm_title', 'Экспорт'],
['core_blocks_export_confirm_with_filters', 'Экспорт будет включать данные со следующими фильтрами:'],
['core_blocks_export_confirm_default', 'Будут экспортированы все данные.'],
['core_blocks_export_change_filters', 'Изменить фильтры'],
['core_blocks_dialog_element_close_tooltip', 'Закрыть панель'],
];
5 changes: 5 additions & 0 deletions webapp/packages/core-blocks/src/locales/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ export default [
['core_blocks_export_image_dialog_title', 'Xuất dưới dạng hình ảnh'],
['core_blocks_export_image_dialog_format', 'Định dạng tệp'],
['core_blocks_export_image_dialog_transparent_background', 'Nền trong suốt'],
['core_blocks_export_title', 'Export to CSV'],
['core_blocks_export_confirm_title', 'Export'],
['core_blocks_export_confirm_with_filters', 'The export will include data with the following filters:'],
['core_blocks_export_confirm_default', 'All data will be exported.'],
['core_blocks_export_change_filters', 'Change Filters'],
['core_blocks_dialog_element_close_tooltip', 'Đóng bảng điều khiển'],
];
5 changes: 5 additions & 0 deletions webapp/packages/core-blocks/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ export default [
['core_blocks_export_image_dialog_title', '导出为图片'],
['core_blocks_export_image_dialog_format', '文件格式'],
['core_blocks_export_image_dialog_transparent_background', '透明背景'],
['core_blocks_export_title', '导出为 CSV'],
['core_blocks_export_confirm_title', '导出'],
['core_blocks_export_confirm_with_filters', '导出将包含带有以下筛选条件的数据:'],
['core_blocks_export_confirm_default', '将导出所有数据。'],
['core_blocks_export_change_filters', '更改筛选条件'],
['core_blocks_dialog_element_close_tooltip', '关闭面板'],
];
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial
import { CreateTeamService } from './Teams/TeamsTable/CreateTeamService.js';
import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService.js';
import { CreateUserService } from './UsersTable/CreateUserService.js';
import type { IUserFilters } from './UsersTable/Filters/useUsersTableFilters.js';

const UserCredentialsList = React.lazy(async () => {
const { UserCredentialsList } = await import('./UsersTable/UserCredentialsList.js');
Expand All @@ -37,11 +38,16 @@ export interface IUserDetailsInfoProps {
user: AdminUser;
}

export interface IUsersExportProps {
filters: IUserFilters;
}

@injectable(() => [AdministrationItemService, CreateUserService, TeamsResource, CreateTeamService, UsersResource, CommonDialogService])
export class UsersAdministrationService extends Bootstrap {
readonly tabsContainer: TabsContainer;
readonly userDetailsInfoPlaceholder: PlaceholderContainer<IUserDetailsInfoProps>;
readonly informationPlaceholder: PlaceholderContainer;
readonly exportPlaceholder: PlaceholderContainer<IUsersExportProps>;
administrationItem!: IAdministrationItem;

constructor(
Expand All @@ -56,6 +62,7 @@ export class UsersAdministrationService extends Bootstrap {
this.userDetailsInfoPlaceholder = new PlaceholderContainer();
this.tabsContainer = new TabsContainer('Access Control');
this.informationPlaceholder = new PlaceholderContainer();
this.exportPlaceholder = new PlaceholderContainer();
}

override register(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@
}

.actions {
display: flex;
align-items: center;
gap: 8px;
}

.buttonBox {
composes: theme-form-element-radius theme-background-surface theme-text-on-surface theme theme-border-color-background from global;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 2px solid;
height: 32px;
overflow: hidden;
}

.button {
Expand Down
Loading
Loading