Merge branch 'devel' into dbeaver/pro#6364-add-option-to-set-default-view-for-project

This commit is contained in:
Sychev Andrey
2025-10-27 10:10:29 +01:00
committed by GitHub
39 changed files with 483 additions and 255 deletions
+6
View File
@@ -24,6 +24,12 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io
[Database access instructions](https://github.com/dbeaver/cloudbeaver/wiki/Demo-Server)
## Changelog
### 25.2.3 2025-10-20
- Added visualization for column reordering in the Data Editor;
- Fixed the critical issue with duplicating the execution of queries executed by a shortcut in the SQL Editor;
- Fixed the critical vulnerability (CVE-2025-61927) in the Happy DOM library. The library was updated to version 20.0.2.
### 25.2.2 2025-10-06
- Replaced true/false text with intuitive checkboxes for boolean values in the metadata editor, making editing easier and faster;
- Keyboard navigation was improved across the application. Additional shortcuts are available in the main info panel (top-right corner). To help you discover them faster, relevant keyboard shortcuts are also shown directly in the tooltips of Data Editor buttons;
@@ -1,6 +1,7 @@
package io.cloudbeaver.model;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.DBPObject;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.connection.DBPDriverConstants;
@@ -52,35 +53,35 @@ public class WebExpertSettingsProperties implements DBPObject {
public static class KeepAliveIntervalFieldValidator implements IPropertyValueValidator<WebExpertSettingsProperties, Object> {
@Override
public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException {
public boolean isValidValue(@NotNull WebExpertSettingsProperties object, @Nullable Object value) throws IllegalArgumentException {
return CommonUtils.toBoolean(!object.driver.isEmbedded(), true);
}
}
public static class AutoCommitFieldValidator implements IPropertyValueValidator<WebExpertSettingsProperties, Object> {
@Override
public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException {
public boolean isValidValue(@NotNull WebExpertSettingsProperties object, @Nullable Object value) throws IllegalArgumentException {
return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_TRANSACTIONS), true);
}
}
public static class ReadOnlyFieldValidator implements IPropertyValueValidator<WebExpertSettingsProperties, Object> {
@Override
public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException {
public boolean isValidValue(@NotNull WebExpertSettingsProperties object, @Nullable Object value) throws IllegalArgumentException {
return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_READ_ONLY_MODE), true);
}
}
public static class DefaultCatalogFieldVisibleValidator implements IPropertyValueValidator<WebExpertSettingsProperties, Object> {
@Override
public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException {
public boolean isValidValue(@NotNull WebExpertSettingsProperties object, @Nullable Object value) throws IllegalArgumentException {
return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_CATALOG_SELECTION), true);
}
}
public static class DefaultSchemaFieldVisibleValidator implements IPropertyValueValidator<WebExpertSettingsProperties, Object> {
@Override
public boolean isValidValue(WebExpertSettingsProperties object, Object value) throws IllegalArgumentException {
public boolean isValidValue(@NotNull WebExpertSettingsProperties object, @Nullable Object value) throws IllegalArgumentException {
return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_SCHEMA_SELECTION), true);
}
}
@@ -41,6 +41,7 @@ import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -204,7 +205,7 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
initializeApplicationServices();
try {
try {
this.instanceServer = new CloudBeaverInstanceServer();
this.instanceServer = createInstanceServer();
} catch (Exception e) {
log.error("Error initializing instance server", e);
}
@@ -216,6 +217,10 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
return EXIT_OK;
}
protected CloudBeaverInstanceServer createInstanceServer() throws IOException {
return new CloudBeaverInstanceServer();
}
protected abstract void startServer() throws DBException;
@Override
@@ -62,6 +62,7 @@ public class ServletSystemInformationCollector<T extends ServletApplication> imp
private DBPConnectionInformation smDatabaseInfo;
private String workspacePath;
private final DeploymentType deploymentType;
private boolean internalDatabaseInfoCollected = false;
public ServletSystemInformationCollector(@NotNull T application) {
this.application = application;
@@ -158,7 +159,15 @@ public class ServletSystemInformationCollector<T extends ServletApplication> imp
/**
* Collects info about internal databases.
*/
public void collectInternalDatabaseUseInformation() throws DBException {
public synchronized void collectInternalDatabaseUseInformation() throws DBException {
if (internalDatabaseInfoCollected) {
return;
}
collectInternalDatabaseInfo();
this.internalDatabaseInfoCollected = true;
}
protected void collectInternalDatabaseInfo() throws DBException {
this.smDatabaseInfo = application.getAdminSecurityController(new NoAuthCredentialsProvider())
.getInternalDatabaseInformation();
}
@@ -16,20 +16,41 @@
*/
package io.cloudbeaver.model.cli;
import org.jkiss.dbeaver.model.cli.ApplicationCommandLine;
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.cli.*;
import org.jkiss.dbeaver.model.cli.model.CommandLineAuthenticator;
import picocli.CommandLine;
public class CloudBeaverCommandLine extends ApplicationCommandLine<ApplicationInstanceController> {
private static CloudBeaverCommandLine INSTANCE = null;
private CloudBeaverCommandLine() {
@Nullable
private final CommandLineAuthenticator authenticator;
public CloudBeaverCommandLine(@Nullable CommandLineAuthenticator authenticator) {
super();
this.authenticator = authenticator;
}
public synchronized static CloudBeaverCommandLine getInstance() {
if (INSTANCE == null) {
INSTANCE = new CloudBeaverCommandLine();
@Override
protected CloudBeaverTopLevelCommand createTopLevelCommand(
@Nullable ApplicationInstanceController applicationInstanceController,
@NotNull CommandLineContext context,
@NotNull CLIRunMeta runMeta
) {
if (authenticator != null) {
context.setContextParameter(CLIConstants.CONTEXT_PARAM_AUTHENTICATOR, authenticator);
}
return INSTANCE;
return new CloudBeaverTopLevelCommand(applicationInstanceController, context, runMeta);
}
@NotNull
@Override
protected CommandLine initCommandLine(
@Nullable ApplicationInstanceController applicationInstanceController,
@NotNull CommandLineContext context,
@NotNull CLIRunMeta runMeta
) {
return super.initCommandLine(applicationInstanceController, context, runMeta);
}
}
@@ -16,7 +16,7 @@
*/
package io.cloudbeaver.model.cli;
import org.apache.commons.cli.CommandLine;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
import org.jkiss.dbeaver.model.cli.ApplicationInstanceServer;
@@ -25,20 +25,27 @@ import org.jkiss.dbeaver.model.cli.CLIProcessResult;
import java.io.IOException;
public class CloudBeaverInstanceServer extends ApplicationInstanceServer<ApplicationInstanceController> {
private final CloudBeaverCommandLine commandLine;
public CloudBeaverInstanceServer() throws IOException {
super(ApplicationInstanceController.class);
this.commandLine = new CloudBeaverCommandLine(null);
}
protected CloudBeaverInstanceServer(@NotNull CloudBeaverCommandLine commandLine) throws IOException {
super(ApplicationInstanceController.class);
this.commandLine = commandLine;
}
@NotNull
@Override
public CLIProcessResult handleCommandLine(@NotNull String[] args) {
CommandLine cmd = CloudBeaverCommandLine.getInstance().getCommandLine(args);
try {
return CloudBeaverCommandLine.getInstance().executeCommandLineCommands(
cmd,
return commandLine.executeCommandLineCommands(
this,
false,
false
false,
args
);
} catch (Exception e) {
return new CLIProcessResult(CLIProcessResult.PostAction.ERROR, "Error executing command: " + e.getMessage());
@@ -0,0 +1,37 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.model.cli;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
import org.jkiss.dbeaver.model.cli.CLIRunMeta;
import org.jkiss.dbeaver.model.cli.CommandLineContext;
import org.jkiss.dbeaver.model.cli.command.AbstractTopLevelCommand;
import picocli.CommandLine;
@CommandLine.Command(name = "cbvr", description = "CloudBeaver commands", mixinStandardHelpOptions = true)
public class CloudBeaverTopLevelCommand extends AbstractTopLevelCommand {
protected CloudBeaverTopLevelCommand(
@Nullable ApplicationInstanceController controller,
@NotNull CommandLineContext context,
@NotNull CLIRunMeta meta
) {
super(controller, context, meta);
}
}
@@ -314,11 +314,6 @@ public abstract class CBApplication<T extends CBServerConfig>
}
grantPermissionsToConnections();
}
try {
this.systemInformationCollector.collectInternalDatabaseUseInformation();
} catch (DBException e) {
log.error("Error collecting system information", e);
}
eventController.scheduleCheckJob();
@@ -20,6 +20,7 @@ package io.cloudbeaver.service.core.impl;
import io.cloudbeaver.*;
import io.cloudbeaver.model.*;
import io.cloudbeaver.model.app.ServletApplication;
import io.cloudbeaver.model.app.ServletSystemInformationCollector;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.registry.WebHandlerRegistry;
import io.cloudbeaver.registry.WebSessionHandlerDescriptor;
@@ -89,10 +90,13 @@ public class WebServiceCore implements DBWServiceCore {
@Override
public WebPropertyInfo[] getSystemInformationProperties(@NotNull WebSession webSession) {
return WebCommonUtils.getObjectProperties(
webSession,
WebAppUtils.getWebApplication().getSystemInformationCollector()
);
ServletSystemInformationCollector<?> collector = WebAppUtils.getWebApplication().getSystemInformationCollector();
try {
collector.collectInternalDatabaseUseInformation();
} catch (DBException e) {
log.error("Error collecting system information", e);
}
return WebCommonUtils.getObjectProperties(webSession, collector);
}
@Override
@@ -233,9 +233,13 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
}
for (String possibleUserId : List.of(userId, userId.toLowerCase())) {
if (isSubjectExists(possibleUserId)) {
log.info("User already exist : " + possibleUserId);
setUserAuthRole(connection, possibleUserId, authRole);
enableUser(connection, possibleUserId, true, null, null);
if (getSubjectType(possibleUserId) == SMSubjectType.team) {
log.error("Cannot import user '%s': a team with this name already exists.".formatted(possibleUserId));
} else {
log.info("User already exist : " + possibleUserId);
setUserAuthRole(connection, possibleUserId, authRole);
enableUser(connection, possibleUserId, true, null, null);
}
continue outer;
}
}
@@ -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'];
+2
View File
@@ -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',
+1
View File
@@ -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`);
}
+1
View File
@@ -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;
});
}
@@ -223,12 +223,12 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
}
if (transaction.changes) {
view.dispatch({ changes: transaction.changes });
view.dispatch({ changes: transaction.changes, annotations: transaction.annotations });
isInitialScriptOpening.current = false;
}
if (transaction.selection) {
view.dispatch({ selection: transaction.selection });
view.dispatch({ selection: transaction.selection, annotations: transaction.annotations });
}
}
});
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* 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.
@@ -13,6 +13,7 @@ import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataMo
export interface IDataValuePanelOptions {
dataFormat: ResultDataFormat[];
isTextPresentation?: boolean;
}
export interface IDataValuePanelProps {
@@ -22,6 +22,9 @@ import ValuePanelTab from './shared/ValuePanelTab.module.css';
import { IDatabaseDataSelectAction } from '../../DatabaseDataModel/Actions/IDatabaseDataSelectAction.js';
import { IDatabaseDataResultAction } from '../../DatabaseDataModel/Actions/IDatabaseDataResultAction.js';
import { IDatabaseDataMetadataAction } from '../../DatabaseDataModel/Actions/IDatabaseDataMetadataAction.js';
import { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js';
import type { IGridDataKey } from '../../DatabaseDataModel/Actions/Grid/IGridDataKey.js';
import { useContentType } from '../../ValuePanelPresentation/TextValue/useContentType.js';
const tabListRegistry: StyleRegistry = [[TabStyles, { mode: 'append', styles: [ValuePanelTab] }]];
@@ -35,7 +38,7 @@ export const ValuePanel: DataPresentationComponent = observer(function ValuePane
const selectAction = model.source.tryGetAction(resultIndex, IDatabaseDataSelectAction);
const dataResultAction = model.source.tryGetAction(resultIndex, IDatabaseDataResultAction);
const metadataAction = model.source.getAction(resultIndex, IDatabaseDataMetadataAction);
const activeElements = selectAction?.getActiveElements();
const activeElements = selectAction?.getActiveElements() as IGridDataKey[] | undefined;
let elementKey: string | null = null;
const style = useS(styles);
@@ -58,13 +61,14 @@ export const ValuePanel: DataPresentationComponent = observer(function ValuePane
);
const displayed = service.getDisplayed({ dataFormat, model, resultIndex });
let currentTabId = state.currentTabId;
const hasCurrentTabCells = currentTabId && displayed.some(tab => tab.key === currentTabId);
if (displayed.length > 0 && !hasCurrentTabCells) {
currentTabId = displayed[0]!.key;
}
const currentTabId = useContentType({
model: model as any,
currentContentType: state.currentTabId,
elementKey: activeElements && activeElements.length > 0 ? activeElements[0] : undefined,
formatAction: model.source.tryGetAction(resultIndex, IDatabaseDataFormatAction),
displayed,
});
return (
<TabsState
@@ -9,6 +9,8 @@
.tabList {
composes: theme-background-secondary theme-border-color-background theme-text-on-secondary from global;
position: relative;
overflow: auto;
&:before {
content: '';
position: absolute;
@@ -8,10 +8,10 @@
import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { ActionIconButton, Container, Group, Loader, s, SContext, type StyleRegistry, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { ActionIconButton, Container, Group, Loader, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { type TabContainerPanelComponent, TabList, TabsState, TabStyles, useTabLocalState } from '@cloudbeaver/core-ui';
import { type TabContainerPanelComponent, useTabLocalState } from '@cloudbeaver/core-ui';
import { ResultSetDataContentAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.js';
import { DataViewerService } from '../../DataViewerService.js';
@@ -19,21 +19,16 @@ import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js';
import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
import { getDefaultLineWrapping } from './getDefaultLineWrapping.js';
import { isTextValueReadonly } from './isTextValueReadonly.js';
import styles from './shared/TextValuePresentation.module.css';
import TextValuePresentationTab from './shared/TextValuePresentationTab.module.css';
import { TextValueEditor } from './TextValueEditor.js';
import { TextValuePresentationService } from './TextValuePresentationService.js';
import { TextValueTruncatedMessage } from './TextValueTruncatedMessage.js';
import { useAutoContentType } from './useAutoContentType.js';
import { useTextValueGetter } from './useTextValueGetter.js';
import { IDatabaseDataSelectAction } from '../../DatabaseDataModel/Actions/IDatabaseDataSelectAction.js';
import { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js';
import { IDatabaseDataEditAction } from '../../DatabaseDataModel/Actions/IDatabaseDataEditAction.js';
import { GridSelectAction } from '../../DatabaseDataModel/Actions/Grid/GridSelectAction.js';
const tabRegistry: StyleRegistry = [[TabStyles, { mode: 'append', styles: [TextValuePresentationTab] }]];
export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelProps> = observer(function TextValuePresentation({
tabId,
model: unknownModel,
resultIndex,
dataFormat,
@@ -44,9 +39,7 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
}
const translate = useTranslate();
const notificationService = useService(NotificationService);
const textValuePresentationService = useService(TextValuePresentationService);
const dataViewerService = useService(DataViewerService);
const style = useS(styles, TextValuePresentationTab);
const selectAction = model.source.getAction(resultIndex, IDatabaseDataSelectAction, GridSelectAction);
const formatAction = model.source.getAction(resultIndex, IDatabaseDataFormatAction);
const activeElements = selectAction.getActiveElements();
@@ -57,33 +50,21 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
const state = useTabLocalState(() =>
observable({
lineWrapping: null as boolean | null,
currentContentType: null as string | null,
setContentType(contentType: string | null) {
this.currentContentType = contentType;
},
setLineWrapping(lineWrapping: boolean | null) {
this.lineWrapping = lineWrapping;
},
}),
);
const contentType = useAutoContentType({
dataFormat,
model,
resultIndex,
currentContentType: state.currentContentType,
elementKey: firstSelectedCell,
formatAction,
});
const textValueGetter = useTextValueGetter({
contentAction,
editAction,
formatAction,
dataFormat,
contentType,
contentType: tabId,
elementKey: firstSelectedCell,
});
const autoLineWrapping = getDefaultLineWrapping(contentType);
const autoLineWrapping = getDefaultLineWrapping(tabId);
const lineWrapping = state.lineWrapping ?? autoLineWrapping;
const isReadonly = isTextValueReadonly({ model, resultIndex, contentAction, cell: firstSelectedCell, formatAction, editAction });
const canSave = firstSelectedCell && contentAction.isDownloadable(firstSelectedCell) && dataViewerService.canExportData;
@@ -106,40 +87,16 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
}
}
function selectTabHandler(tabId: string) {
// currentContentType may be selected automatically we don't want to change state in this case
if (tabId !== contentType) {
state.setContentType(tabId);
}
}
function toggleLineWrappingHandler() {
state.setLineWrapping(!lineWrapping);
}
return (
<Container vertical gap dense overflow>
<Container keepSize center overflow>
<Container keepSize>
<TabsState
dataFormat={dataFormat}
resultIndex={resultIndex}
container={textValuePresentationService.tabs}
currentTabId={contentType}
model={model}
lazy
onChange={tab => selectTabHandler(tab.tabId)}
>
<SContext registry={tabRegistry}>
<TabList className={s(style, { tabList: true, textValuePresentationTab: true, underline: true })} underline />
</SContext>
</TabsState>
</Container>
</Container>
<Loader suspense>
<Group overflow maximum box>
<TextValueEditor
contentType={contentType}
contentType={tabId}
lineWrapping={lineWrapping}
readonly={isReadonly}
valueGetter={textValueGetter}
@@ -1,87 +1,82 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* 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 React from 'react';
import { importLazyComponent } from '@cloudbeaver/core-blocks';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { isResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js';
import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
import { DataValuePanelService, type IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
import { isBlobPresentationAvailable } from './isTextValuePresentationAvailable.js';
import { TextValuePresentationService } from './TextValuePresentationService.js';
const TextValuePresentation = importLazyComponent(() => import('./TextValuePresentation.js').then(module => module.TextValuePresentation));
@injectable(() => [TextValuePresentationService, DataValuePanelService])
const PRESENTATIONS = [
{
key: 'text/plain',
name: 'data_viewer_presentation_value_text_title',
},
{
key: 'text/html',
name: 'data_viewer_presentation_value_text_html_title',
},
{
key: 'text/xml',
name: 'data_viewer_presentation_value_text_xml_title',
},
{
key: 'application/json',
name: 'data_viewer_presentation_value_text_json_title',
},
{
key: 'application/octet-stream;type=hex',
name: 'data_viewer_presentation_value_text_hex_title',
isHidden: (context: IDataValuePanelProps | undefined) => !isBlobPresentationAvailable(context),
},
{
key: 'application/octet-stream;type=base64',
name: 'data_viewer_presentation_value_text_base64_title',
isHidden: (context: IDataValuePanelProps | undefined) => !isBlobPresentationAvailable(context),
},
];
@injectable(() => [DataValuePanelService])
export class TextValuePresentationBootstrap extends Bootstrap {
constructor(
private readonly textValuePresentationService: TextValuePresentationService,
private readonly dataValuePanelService: DataValuePanelService,
) {
constructor(private readonly dataValuePanelService: DataValuePanelService) {
super();
}
override register(): void {
this.dataValuePanelService.add({
key: 'text-presentation',
options: {
dataFormat: [ResultDataFormat.Resultset],
},
name: 'data_viewer_presentation_value_text_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => TextValuePresentation,
isHidden(_, props) {
return !props || !props.model.source.hasResult(props.resultIndex) || !isResultSetDataSource(props.model.source);
},
});
for (const presentation of PRESENTATIONS) {
this.dataValuePanelService.add({
key: presentation.key,
options: {
dataFormat: [ResultDataFormat.Resultset],
isTextPresentation: true,
},
name: presentation.name,
order: Number.MAX_SAFE_INTEGER,
panel: () => TextValuePresentation,
isHidden: (_, props) => {
if (!props || !props.model.source.hasResult(props.resultIndex) || !isResultSetDataSource(props.model.source)) {
return true;
}
this.textValuePresentationService.add({
key: 'text/plain',
name: 'data_viewer_presentation_value_text_plain_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
});
this.textValuePresentationService.add({
key: 'text/html',
name: 'data_viewer_presentation_value_text_html_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
// isHidden: (_, context) => isBlobPresentationAvailable(context),
});
this.textValuePresentationService.add({
key: 'text/xml',
name: 'data_viewer_presentation_value_text_xml_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
// isHidden: (_, context) => isBlobPresentationAvailable(context),
});
this.textValuePresentationService.add({
key: 'application/json',
name: 'data_viewer_presentation_value_text_json_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
// isHidden: (_, context) => isBlobPresentationAvailable(context),
});
if (!presentation.isHidden) {
return false;
}
this.textValuePresentationService.add({
key: 'application/octet-stream;type=hex',
name: 'data_viewer_presentation_value_text_hex_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
isHidden: (_, context) => !isBlobPresentationAvailable(context),
});
this.textValuePresentationService.add({
key: 'application/octet-stream;type=base64',
name: 'data_viewer_presentation_value_text_base64_title',
order: Number.MAX_SAFE_INTEGER,
panel: () => React.Fragment,
isHidden: (_, context) => !isBlobPresentationAvailable(context),
});
return presentation.isHidden({
dataFormat: props.dataFormat,
model: props.model,
resultIndex: props.resultIndex,
});
},
});
}
}
}
@@ -1,34 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { type ITabInfo, type ITabInfoOptions, TabsContainer } from '@cloudbeaver/core-ui';
import { type IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js';
import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js';
import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
export interface ITextValuePanelProps extends Omit<IDataValuePanelProps, 'model'> {
model: IDatabaseDataModel<ResultSetDataSource>;
}
@injectable()
export class TextValuePresentationService {
readonly tabs: TabsContainer<ITextValuePanelProps, IDataValuePanelOptions>;
constructor() {
this.tabs = new TabsContainer('Value presentation');
}
get(tabId: string): ITabInfo<ITextValuePanelProps, IDataValuePanelOptions> | undefined {
return this.tabs.getTabInfo(tabId);
}
add(tabInfo: ITabInfoOptions<ITextValuePanelProps, IDataValuePanelOptions>): void {
this.tabs.add(tabInfo);
}
}
@@ -9,9 +9,9 @@ import { isResultSetBinaryValue } from '@dbeaver/result-set-api';
import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js';
import { ResultSetSelectAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.js';
import { ResultSetViewAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.js';
import { type ITextValuePanelProps } from './TextValuePresentationService.js';
import type { IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
export function isBlobPresentationAvailable(context: ITextValuePanelProps | undefined): boolean {
export function isBlobPresentationAvailable(context: IDataValuePanelProps | undefined): boolean {
const source = context?.model.source;
if (!context || !source?.hasResult(context.resultIndex)) {
return true;
@@ -1,13 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.tabList {
composes: theme-border-color-background theme-background-background from global;
overflow: auto;
border-radius: var(--theme-group-element-radius);
}
@@ -1,18 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.tab {
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global;
}
.textValuePresentationTab.underline .tab {
border-bottom: 0;
&:global([aria-selected='false']) {
border-bottom: 0 !important;
}
}
@@ -5,25 +5,23 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useService } from '@cloudbeaver/core-di';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
import type { ITabInfo } from '@cloudbeaver/core-ui';
import { isResultSetContentValue } from '@dbeaver/result-set-api';
import { isResultSetBlobValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetBlobValue.js';
import type { IResultSetValue } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.js';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js';
import { ResultSetDataSource } from '../../ResultSet/ResultSetDataSource.js';
import { TextValuePresentationService } from './TextValuePresentationService.js';
import type { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js';
import type { IGridDataKey } from '../../DatabaseDataModel/Actions/Grid/IGridDataKey.js';
import { isResultSetDataModel } from '../../ResultSet/isResultSetDataModel.js';
import type { IDatabaseDataSource } from '../../DatabaseDataModel/IDatabaseDataSource.js';
import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
interface Args {
resultIndex: number;
model: IDatabaseDataModel<ResultSetDataSource>;
dataFormat: ResultDataFormat | null;
currentContentType: string | null;
interface UseAutoContentTypeArgs {
model: IDatabaseDataModel<IDatabaseDataSource>;
elementKey?: IGridDataKey;
formatAction: IDatabaseDataFormatAction;
formatAction?: IDatabaseDataFormatAction;
displayed: Array<ITabInfo<IDataValuePanelProps, IDataValuePanelOptions>>;
}
const DEFAULT_CONTENT_TYPE = 'text/plain';
@@ -55,24 +53,16 @@ function preprocessDefaultContentType(contentType: string | null | undefined) {
return DEFAULT_CONTENT_TYPE;
}
export function useAutoContentType({ dataFormat, model, formatAction, resultIndex, currentContentType, elementKey }: Args) {
const textValuePresentationService = useService(TextValuePresentationService);
const activeTabs = textValuePresentationService.tabs.getDisplayed({
dataFormat: dataFormat,
model,
resultIndex: resultIndex,
});
export function useAutoContentType({ displayed, formatAction, model, elementKey }: UseAutoContentTypeArgs): string | null {
const isAllTextType = displayed.every(tab => tab.options?.isTextPresentation);
const isAutoType = isAllTextType && formatAction && isResultSetDataModel(model);
if (!isAutoType) {
return null;
}
const contentValue = elementKey ? formatAction.get(elementKey) : null;
const contentValueType = getContentTypeFromResultSetValue(contentValue);
const defaultContentType = preprocessDefaultContentType(contentValueType);
if (currentContentType === null) {
currentContentType = defaultContentType;
}
if (activeTabs.length > 0 && !activeTabs.some(tab => tab.key === currentContentType)) {
currentContentType = activeTabs[0]!.key;
}
return currentContentType;
return preprocessDefaultContentType(contentValueType);
}
@@ -0,0 +1,40 @@
/*
* 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 type { ITabInfo } from '@cloudbeaver/core-ui';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel.js';
import type { IDatabaseDataFormatAction } from '../../DatabaseDataModel/Actions/IDatabaseDataFormatAction.js';
import type { IGridDataKey } from '../../DatabaseDataModel/Actions/Grid/IGridDataKey.js';
import type { IDatabaseDataSource } from '../../DatabaseDataModel/IDatabaseDataSource.js';
import type { IDataValuePanelOptions, IDataValuePanelProps } from '../../TableViewer/ValuePanel/DataValuePanelService.js';
import { useAutoContentType } from './useAutoContentType.js';
interface UseContentTypeArgs {
model: IDatabaseDataModel<IDatabaseDataSource>;
currentContentType: string | null;
elementKey?: IGridDataKey;
formatAction?: IDatabaseDataFormatAction;
displayed: Array<ITabInfo<IDataValuePanelProps, IDataValuePanelOptions>>;
}
const DEFAULT_CONTENT_TYPE = 'text/plain';
export function useContentType({ model, formatAction, currentContentType, elementKey, displayed }: UseContentTypeArgs): string {
const autoContentType = useAutoContentType({ displayed, formatAction, model, elementKey });
if (displayed.length === 0) {
return currentContentType || DEFAULT_CONTENT_TYPE;
}
currentContentType = currentContentType || autoContentType;
const hasCurrentTab = currentContentType && displayed.some(tab => tab.key === currentContentType);
if (!hasCurrentTab) {
currentContentType = currentContentType || displayed[0]!.key;
}
return currentContentType || DEFAULT_CONTENT_TYPE;
}
@@ -8,7 +8,6 @@
import { Bootstrap, conditional, Dependency, external, ModuleRegistry, proxy } from '@cloudbeaver/core-di';
import { TextValuePresentationBootstrap } from './ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.js';
import { TextValuePresentationService } from './ValuePanelPresentation/TextValue/TextValuePresentationService.js';
import { ImageValuePresentationBootstrap } from './ValuePanelPresentation/ImageValue/ImageValuePresentationBootstrap.js';
import { BooleanValuePresentationBootstrap } from './ValuePanelPresentation/BooleanValue/BooleanValuePresentationBootstrap.js';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService.js';
@@ -72,7 +71,6 @@ export default ModuleRegistry.add({
.addSingleton(Dependency, proxy(DataViewerSettingsService))
.addSingleton(DataPresentationService)
.addSingleton(TextValuePresentationBootstrap)
.addSingleton(TextValuePresentationService)
.addSingleton(ImageValuePresentationBootstrap)
.addSingleton(BooleanValuePresentationBootstrap)
.addSingleton(TableViewerStorageService)
+11 -3
View File
@@ -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"
@@ -12354,13 +12355,13 @@ __metadata:
linkType: hard
"happy-dom@npm:^20":
version: 20.0.0
resolution: "happy-dom@npm:20.0.0"
version: 20.0.2
resolution: "happy-dom@npm:20.0.2"
dependencies:
"@types/node": "npm:^20.0.0"
"@types/whatwg-mimetype": "npm:^3.0.2"
whatwg-mimetype: "npm:^3.0.0"
checksum: 10c0/a205da48f3c98ae2b2bcb73c8f673fea66296cd2fc081065fcd5fa42b4411546c1b8f7c26ef2ca5adf0bdd1ced9aff4f429a3a4f5e6bce7724eec2747bef1a49
checksum: 10c0/d4449e087cb7566fb493f9e6ee99221c64177732bf944ec3e2f464033107832c637e201e3e78e81a8b35871d41aa510806f3f1a2f18b924fc6120226f337a845
languageName: node
linkType: hard
@@ -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"