mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
[CB-5925] Charts - add export option (image) (#3819)
* dbeaver/pro#5078 adds chart export
* adds ExportImageDialog and reuses it across the app
* uses canvas2html for downloading images
* Revert "uses canvas2html for downloading images"
This reverts commit b59374cf85.
* moves transformClassesToStyles to helpers
* transforms styling attributes to computed styles
* adds file name
* fixes translations
* fixes styles for png image so it do not exports in black&white mode with incorrect sizing
* fixes styles for svg export
* uses domToImage library to save chart with legend
* cleanup
* moves helpers back to d3js diagrams
* cleanup
* uses html-to-image instead of dom-to-image
* cleanup
* pr fixes
* replaces the lib
* fixes svg so it is not blur
* validation for image urls
* cleanup
---------
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgenia
parent
046659672c
commit
37cfecbf91
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 { observable } from 'mobx';
|
||||
import { EXPORT_IMAGE_FORMATS, type ExportImageFormat } from './ExportImageFormats.js';
|
||||
import { useTranslate } from '../localization/useTranslate.js';
|
||||
import { useObservableRef } from '../useObservableRef.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 { ErrorMessage } from '../ErrorMessage.js';
|
||||
import { FieldCheckbox } from '../FormControls/Checkboxes/FieldCheckbox.js';
|
||||
import { InputField } from '../FormControls/InputField/InputField.js';
|
||||
import { useErrorDetails } from '../useErrorDetails.js';
|
||||
import { Select } from '../FormControls/Select.js';
|
||||
import { Button } from '../Button.js';
|
||||
|
||||
export interface IExportImageOptions {
|
||||
format: ExportImageFormat;
|
||||
transparent: boolean;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface ExportImagePayload {
|
||||
export: (options: IExportImageOptions) => Promise<void>;
|
||||
defaultFileName: string;
|
||||
}
|
||||
|
||||
interface State extends IExportImageOptions {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export const ExportImageDialog = observer<DialogComponentProps<ExportImagePayload, null>>(function ExportImageDialog(props) {
|
||||
const translate = useTranslate();
|
||||
const state = useObservableRef<State>(
|
||||
() => ({
|
||||
format: 'SVG',
|
||||
transparent: false,
|
||||
fileName: props.payload.defaultFileName,
|
||||
error: null,
|
||||
}),
|
||||
{
|
||||
format: observable.ref,
|
||||
transparent: observable.ref,
|
||||
fileName: observable.ref,
|
||||
error: observable.ref,
|
||||
},
|
||||
false,
|
||||
);
|
||||
const errorDetails = useErrorDetails(state.error);
|
||||
|
||||
async function onExportHandler() {
|
||||
try {
|
||||
await props.payload.export(state);
|
||||
props.resolveDialog();
|
||||
} catch (error: any) {
|
||||
state.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonDialogWrapper size="small">
|
||||
<CommonDialogHeader title="core_blocks_export_image_dialog_title" onReject={props.rejectDialog} />
|
||||
<CommonDialogBody>
|
||||
<Container>
|
||||
<Container small gap>
|
||||
<InputField state={state} name="fileName">
|
||||
{translate('ui_name')}
|
||||
</InputField>
|
||||
<Select items={EXPORT_IMAGE_FORMATS} keySelector={value => value} valueSelector={value => value} state={state} name="format">
|
||||
{translate('core_blocks_export_image_dialog_format')}
|
||||
</Select>
|
||||
<FieldCheckbox state={state} name="transparent" label={translate('core_blocks_export_image_dialog_transparent_background')} />
|
||||
</Container>
|
||||
</Container>
|
||||
{errorDetails.name && (
|
||||
<ErrorMessage text={errorDetails.message || errorDetails.name} hasDetails={errorDetails.hasDetails} onShowDetails={errorDetails.open} />
|
||||
)}
|
||||
</CommonDialogBody>
|
||||
<CommonDialogFooter className="tw:justify-end tw:items-center tw:gap-[24px]">
|
||||
<Button type="button" variant="secondary" onClick={props.rejectDialog}>
|
||||
{translate('app_shared_inlineEditor_dialog_cancel')}
|
||||
</Button>
|
||||
<Button type="button" onClick={onExportHandler}>
|
||||
{translate('ui_export')}
|
||||
</Button>
|
||||
</CommonDialogFooter>
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 ExportImageDialog = importLazyComponent(() => import('./ExportImageDialog.js').then(m => m.ExportImageDialog));
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 type ExportImageFormat = 'SVG' | 'PNG';
|
||||
|
||||
export const EXPORT_IMAGE_FORMATS: ExportImageFormat[] = ['SVG', 'PNG'];
|
||||
@@ -19,6 +19,8 @@ export { default as ConfirmationDialogStyles } from './CommonDialog/Confirmation
|
||||
export * from './CommonDialog/ConfirmationDialogDelete.js';
|
||||
export * from './CommonDialog/RenameDialog.js';
|
||||
export * from './CommonDialog/DialogsPortal.js';
|
||||
export * from './ExportImageDialog/ExportImageDialogLazy.js';
|
||||
export * from './ExportImageDialog/ExportImageFormats.js';
|
||||
|
||||
export * from './ErrorDetailsDialog/ErrorDetailsDialog.js';
|
||||
|
||||
|
||||
@@ -13,4 +13,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', 'Unable to display content'],
|
||||
['core_blocks_exception_message_error_message', 'Something went wrong.'],
|
||||
['core_blocks_custom_input_validation_error', 'Field value is not valid'],
|
||||
['core_blocks_export_image_dialog_title', 'Export Image'],
|
||||
['core_blocks_export_image_dialog_format', 'File format'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Transparent background'],
|
||||
];
|
||||
|
||||
@@ -12,4 +12,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', "Impossible d'afficher le contenu"],
|
||||
['core_blocks_exception_message_error_message', 'Quelque chose a mal tourné.'],
|
||||
['core_blocks_custom_input_validation_error', "La valeur du champ n'est pas valide"],
|
||||
['core_blocks_export_image_dialog_title', "Exporter l'image"],
|
||||
['core_blocks_export_image_dialog_format', 'Format de fichier'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Fond transparent'],
|
||||
];
|
||||
|
||||
@@ -7,4 +7,7 @@ export default [
|
||||
['core_blocks_object_property_info_form_empty_placeholder', 'Properties empty'],
|
||||
['core_blocks_properties_table_item_remove', 'Remove property'],
|
||||
['core_blocks_custom_input_validation_error', 'Field value is not valid'],
|
||||
['core_blocks_export_image_dialog_title', 'Esporta immagine'],
|
||||
['core_blocks_export_image_dialog_format', 'Formato file'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Sfondo trasparente'],
|
||||
];
|
||||
|
||||
@@ -10,4 +10,7 @@ export default [
|
||||
['combobox_no_results_placeholder', 'Нет совпадений'],
|
||||
['combobox_select_placeholder', 'Выберите опцию'],
|
||||
['core_blocks_custom_input_validation_error', 'Значение не валидно'],
|
||||
['core_blocks_export_image_dialog_title', 'Экспорт изображения'],
|
||||
['core_blocks_export_image_dialog_format', 'Формат файла'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Прозрачный фон'],
|
||||
];
|
||||
|
||||
@@ -13,4 +13,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', 'Không thể hiển thị nội dung'],
|
||||
['core_blocks_exception_message_error_message', 'Đã xảy ra lỗi.'],
|
||||
['core_blocks_custom_input_validation_error', 'Giá trị trường không hợp lệ'],
|
||||
['core_blocks_export_image_dialog_title', 'Xuất 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'],
|
||||
];
|
||||
|
||||
@@ -13,4 +13,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', '无法显示内容'],
|
||||
['core_blocks_exception_message_error_message', '出现错误.'],
|
||||
['core_blocks_custom_input_validation_error', '字段值无效'],
|
||||
['core_blocks_export_image_dialog_title', '导出图像'],
|
||||
['core_blocks_export_image_dialog_format', '文件格式'],
|
||||
['core_blocks_export_image_dialog_transparent_background', '透明背景'],
|
||||
];
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = [
|
||||
'joint',
|
||||
'backbone',
|
||||
'lodash',
|
||||
'canvg',
|
||||
'modern-screenshot',
|
||||
'dagre',
|
||||
'svg-pathdata',
|
||||
'@emotion',
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"fast-deep-equal": "^3",
|
||||
"md5": "^2",
|
||||
"mobx": "^6",
|
||||
"modern-screenshot": "^4",
|
||||
"tslib": "^2",
|
||||
"underscore": "^1",
|
||||
"uuid": "^13",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 * as modernScreenshot from 'modern-screenshot';
|
||||
import type { Options } from 'modern-screenshot';
|
||||
import { download } from './download.js';
|
||||
import { isImageBroken } from './isBrokenImage.js';
|
||||
|
||||
export type { Options as IScreenshotOptions };
|
||||
|
||||
const BROKEN_IMAGE_ERROR_MESSAGE = 'Something went wrong. please try select another file format';
|
||||
|
||||
export async function downloadSvg<T extends Node>(element: T, options: Options, fileName: string): Promise<void> {
|
||||
const svg = await modernScreenshot.domToForeignObjectSvg<T>(element, options);
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob(['<?xml version="1.0" standalone="no"?>\r\n', svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const uri = URL.createObjectURL(blob);
|
||||
const isBrokenImage = await isImageBroken(uri);
|
||||
|
||||
if (isBrokenImage) {
|
||||
throw new Error(BROKEN_IMAGE_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
download(blob, `${fileName}.svg`);
|
||||
}
|
||||
|
||||
export async function downloadPng<T extends Node>(element: T, options: Options, fileName: string): Promise<void> {
|
||||
const png = await modernScreenshot.domToPng(element, options);
|
||||
const isBrokenImage = await isImageBroken(png);
|
||||
|
||||
if (isBrokenImage) {
|
||||
throw new Error(BROKEN_IMAGE_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
download(png, `${fileName}.png`);
|
||||
}
|
||||
@@ -89,3 +89,4 @@ export * from './bindFunctions.js';
|
||||
export * from './getDomainFromUrl.js';
|
||||
export * from './isNumber.js';
|
||||
export * from './getSubjectDifferences.js';
|
||||
export * from './downloadImage.js';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 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 function isImageBroken(imageUrl: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
@@ -2230,6 +2230,7 @@ __metadata:
|
||||
fast-deep-equal: "npm:^3"
|
||||
md5: "npm:^2"
|
||||
mobx: "npm:^6"
|
||||
modern-screenshot: "npm:^4"
|
||||
rimraf: "npm:^6"
|
||||
tslib: "npm:^2"
|
||||
typescript: "npm:^5"
|
||||
@@ -15160,6 +15161,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"modern-screenshot@npm:^4":
|
||||
version: 4.6.6
|
||||
resolution: "modern-screenshot@npm:4.6.6"
|
||||
checksum: 10c0/0632cdd89db1a7c0b2d8e78d3e05daca77a62d14ee172f6480da3b2de97fd37938c6ed71041271014a601fe2bcb470d01a78c10456ef7ac59a8674b82c64d0a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:^2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
|
||||
Reference in New Issue
Block a user