Compare commits

...
Author SHA1 Message Date
Viktor KirstandMashaKorax 446bd81e08 dbeaver/pro#8392 Add H2 2.4 JDBC driver (#4227)
* dbeaver/pro#8392 Add H2 2.4 JDBC driver

* dbeaver/pro#8392 Fixed h2 driver description

* dbeaver/pro#8392 Add H2 V.2.4 module

* dbeaver/pro#8392 Fixed module name

* dbeaver/pro#8392 Rename H2 v2.4 driver to v3

---------

Co-authored-by: MashaKorax <84867187+MashaKorax@users.noreply.github.com>
2026-03-31 17:28:50 +02:00
Dmitrii Barnukov 09863ea260 dbeaver/pro#7963 extended openurl event, added client event (#4247)
* dbeaver/pro#7963 extended openurl event, added client event

* dbeaver/pro#7963 fix typo
2026-03-31 16:52:46 +02:00
sergeyteleshev 34b4556773 [CB] New and repeat passwords can be different (#4234)
* dbeaver/pro#8572 fixes password validity on blur

* Revert "dbeaver/pro#8572 fixes password validity on blur"

This reverts commit cefd174254.

* adds context to the form for correct validation + handles validation on change also to keep the actual validity state

* adds context check to prevent validation inconsistent behavior

* adds revalidate to handle depended validations cases

* adds useFormCustomInputValidation

* cleanup

* uses useFormCustomInputValidation to avoid regression after refactor
2026-03-31 14:07:47 +02:00
alexandmr-anton-t 9b4c874e51 dbeaver/pro#8805 do not move focus on programmatic tab select (#4248)
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-03-31 14:00:57 +02:00
Serge Rider ddbc9789e7 Merge remote-tracking branch 'origin/devel' into devel 2026-03-30 16:43:02 +02:00
Serge Rider 810d0215cb dbeaver#40616 update sql server driver to 13.4.0 2026-03-30 16:42:39 +02:00
alexandEvgenia 3dadc17ff1 dbeaver/pro#8644 add temp icon for base sql tab (#4243)
* dbeaver/pro#8644 add temp icon for base sql tab

* dbeaver/pro#8644 replace old icons

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-03-30 12:20:27 +02:00
sergeyteleshevandEvgenia 8c69985083 dbeaver/pro#8518 adds message for string parameters (#4233)
* dbeaver/pro#8518 adds message for string parameters

* aligned message with dbeaver: "string values must be quoted"

* reverts branding and uses it optionally for bind parameters popup

* renames prop

* makes few versions of alert

* Revert "makes few versions of alert"

This reverts commit 5b9f09009e.

* adds gray message

* adds icon

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-03-27 19:04:39 +01:00
1d5b475482 dbeaver/pro#8553 sql execution plan diagram (#4218)
* dbeaver/pro#8553 feat: add execution plan diagram component

* dbeaver/pro#8553 feat: enhance ExecutionPlanDiagram component with selectedNodeId prop

* dbeaver/pro#8553 feat: add flex-shrink to metric value for better layout

* dbeaver/pro#8553 refactor: remove queryString from IPlanData interface

* dbeaver/pro#8553 feat: integrate execution plan diagram component

* dbeaver/pro#8553 refactor: execution plan diagram

separate state, add l10n, make toolbar composable

* dbeaver/pro#8553 feat: implement keyboard navigation for execution plan nodes

* dbeaver/pro#8553 feat: add translation support for execution plan diagram labels

* dbeaver/pro#8553 fix: execution plan node dimensions

* dbeaver/pro#8553 refactor: move formatNumber to helpers to use it

* dbeaver/pro#8553 feat: add translation support for execution plan diagram API

* dbeaver/pro#8553 docs: add README and API documentation for execution plan diagram component

* dbeaver/pro#8553 feat: adjust cost bar height and collapse indicator position

* dbeaver/pro#8553 feat: update PRIMARY_AXIS_WEIGHT for improved directional navigation

* dbeaver/pro#8553 feat: refactor diagram actions and add collapse state management

* dbeaver/pro#8553 refactor: move execution plan diagram to separate EE plugin

* dbeaver/pro#8553 docs: remove redundant comment

* dbeaver/pro#8553 docs: update comments for source and target anchors in ILayoutEdge interface

* dbeaver/pro#8553 docs: update comment in ILayoutNode interface

* dbeaver/pro#8553 refactor: simplify heavy route computation

* dbeaver/pro#8553 refactor: optimize node handling in computePlanLayout function

* dbeaver/pro#8553 refactor: improve tree structure handling in usePlanTree and buildTree functions

* dbeaver/pro#8553 refactor: simplify usePlanTree

* dbeaver/pro#8553 refactor: extract actions from useExecutionPlanDiagramState

* dbeaver/pro#8553 refactor: improve configuration in execution plan diagram

* dbeaver/pro#8553 refactor: simplify metric rendering in PlanNode component

* dbeaver/pro#8553 feat: collapse by hotkey, not an extra element

* dbeaver/pro#8553 feat: add tooltip hints for plan node interactions

* dbeaver/pro#8553 refactor: remove high cost calculation and add node kind attribute

* dbeaver/pro#8553 style: fix formatting in index.ts for ISqlExecutionPlanViewProps export

* dbeaver/pro#8553 ci: remove unused dependency on @dbeaver/ui-kit

* dbeaver/pro#8553 refactor: remove SQL execution plan diagram translations from locale files

* dbeaver/pro#8553 refactor: update translation keys for SQL execution plan components

* dbeaver/pro#8553 refactor: rename components

* dbeaver/pro#8553 style: const

* dbeaver/pro#8553 refactor: reset execution plan diagram state properties

* dbeaver/pro#8553 refactor: update component imports and extract a component

* dbeaver/pro#8553 refactor: rename PlanNodeKind to EPlanNodeKind and update references

* dbeaver/pro#8553 feat: add zoom icons and update action references

* dbeaver/pro#8553 refactor: enhance translation functionality by adding args interpolation

* dbeaver/pro#8553 fix: Polynomial regular expression used on uncontrolled data

* dbeaver/pro#8553 refactor: update ActionsCapture to use ref for toolbar actions

* dbeaver/pro#8553 refactor: use jsx

* dbeaver/pro#8553 refactor: simplify

* dbeaver/pro#8553 refactor: replace useContext with use in DiagramContext

* dbeaver/pro#8553 refactor: convert ExecutionPlanEdge to memoized component

* dbeaver/pro#8553 refactor: convert ExecutionPlanNode to memoized component

* dbeaver/pro#8553 refactor: unify ref prop in ExecutionPlanNode and ExecutionPlanViewport

* dbeaver/pro#8553 refactor: extract ExecutionPlanViewportContent component and clean up CSS

* dbeaver/pro#8553 refactor: convert ExecutionPlanNodeMetric to memoized component

* dbeaver/pro#8553 refactor: improve usePlanLayout hook by utilizing useEffectEvent for stable callback

* dbeaver/pro#8553 refactor: simplify diagram actions handling in createExecutionPlanDiagram

* dbeaver/pro#8553 refactor:  move out execution plan diagram component from

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: alex <48489896+devnaumov@users.noreply.github.com>
2026-03-27 16:07:31 +01:00
sergeyteleshev b779b9d855 [CB] Fix high CVE-2026-33671 in picomatch (#4244)
* dbeaver/pro#8836 fixes picomatch CVE

* fixes happy-dom CVE
2026-03-27 14:24:43 +01:00
Sychev Andrey df676913a0 dbeaver/pro#8808 feat: add shortcut for search in data viewer (#4239)
* dbeaver/pro#8808 feat: add shortcut for search in data viewer

* dbeaver/pro#8808 feat: update shortcut ID for search functionality
2026-03-27 14:22:14 +01:00
fab9d1dd43 dbeaver/pro#7806 remove MenuBarSmallItem (#4224)
* dbeaver/pro#7806 remove MenuBarSmallItem

* dbeaver/pro#7806 remove reakit

* dbeaver/pro#7806 remove extra class

* dbeaver/pro#7806 increase base padding and background color

* dbeaver/pro#7806 use more safe style

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-03-27 14:18:35 +01:00
36dfdc8b33 dbeaver/pro#8825 Add tests for translating SQL CREATE/ALTER TABLE sta… (#4237)
* dbeaver/pro#8825 Add tests for translating SQL CREATE/ALTER TABLE statements with ASCII columns

* dbeaver/pro#8825 improve script normalization

* dbeaver/pro#8825 Update `SQLQueryTranslatorTest` to include `NOT NULL` constraint for ASCII column validation

---------

Co-authored-by: Ivan Gagarkin <gagarkin@Ivans-MacBook-Pro.local>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-03-25 13:51:45 +01:00
Alexander Skoblikovandkseniaguzeeva 8b0a20cfd7 dbeaver/pro#8768 fix federated logout (#4230)
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2026-03-25 08:22:55 +01:00
Sychev AndreyandEvgenia 9553c30ee5 dbeaver/pro#8739 feat: add isEnabled option to tree filter for enhanc… (#4236)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-03-24 19:26:36 +01:00
Sychev AndreyandDaria Marutkina 9c36651aa0 dbeaver/pro#8760 feat: set max width for search panel in SQL editor (#4231)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-03-24 16:44:14 +01:00
sergeyteleshev 24a251cd5b [CB] Fix high CVE-2026-33228 in flatted (2) (#4228) 2026-03-24 16:43:43 +01:00
alexandEvgenia a44ba89cd9 dbeaver/pro#8339 show array value as array (#4229)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-03-24 15:00:59 +01:00
44077ad27a dbeaver/pro#8260 Provide a method to create or restore a WebSession… (#4222)
* dbeaver/pro#8260 Provide a method to create or restore a `WebSession` from `smAccessToken`

* dbeaver/pro#8260 Annotations and constants

---------

Co-authored-by: Ivan Gagarkin <gagarkin@Ivans-MacBook-Pro.local>
Co-authored-by: Serge Rider <serge@jkiss.org>
Co-authored-by: serge-rider <serge@dbeaver.com>
2026-03-24 10:56:19 +01:00
Dmitrii BarnukovandEvgenia 3ae74763ff dbeaver/pro#8635 added message description (#4220)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-03-23 16:42:49 +01:00
Daria Marutkina 45a63f70ee dbeaver/pro#8732 Update README file (#4232)
Added detailed changelog for version 26.0.1, including updates to Data Editor, SQL Editor, general improvements, driver updates, and security fixes.
2026-03-23 12:42:04 +01:00
1bafe29e99 dbeaver/pro#7696 feat: replace slide component with dialog for improv… (#4192)
* dbeaver/pro#7696 feat: replace slide component with dialog for improved UI interaction

* dbeaver/pro#7696 refactor: update persistent elements getter

* dbeaver/pro#7696 fix: notifications persistence

* dbeaver/pro#7696 fix: add min-width to container in RightArea and Administration CSS

* dbeaver/pro#7696 refactor: remove persist prop from SideBarPanel component

make SideBarPanel persist by default

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-03-19 18:11:31 +01:00
Dmitrii Barnukov 10ad601f8b dbeaver/pro#5562 revert hide password if not applicable (#4225) 2026-03-19 10:31:47 +01:00
157 changed files with 1283 additions and 1504 deletions
+29
View File
@@ -25,6 +25,35 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io
## Changelog
### 26.0.1 2026-03-23
- Data Editor:
- Added the Find and Replace functionality for the Data Editor with the ability to find data by matching case, whole word, or using regular expressions.
- Added support for adding, duplicating, deleting, or setting values to NULL in multiple highlighted rows via the Data Editor context menu.
- SQL query generation in the Data Editor is now available for columns containing large object data.
- Improved column drag-and-drop behavior in the Data Editor. Users could drag columns from any area within the header and drop them onto any part of the target column or table body.
- SQL Editor:
- Added fuzzy search to SQL Editor auto-completion for object names. The feature allowed users to find tables and containers even with typos or incorrect letter ordering by matching strings that start with, contain, or resemble the input.
- Fixed data editing for SQL scripts with variables.
- General:
- Added a close button to the right tab when it is the only open tab (ex. Scripts). Users no longer needed to use the main menu to close a single tab.
- Replaced the generic "Something went wrong" message with a specific network error notification when the application cannot connect to the server.
- Null or empty values can now be assigned to the Driver properties for all drivers.
- Fixed an issue where re-selecting an existing SSH key file during connection edits caused problems.
- Fixed the ability to switch tabs in the Data editor using the keyboard.
- Fixed error notification spelling. Thanks to @Malcolm-B-Breaks.
- ClickHouse:
- Updated driver version to 0.9.7.
- Fixed IP address display in the Data Editor by removing the leading slash.
- PostgreSQL: Fixed an error when connecting via URL if the database name contains a hyphen.
- Security:
- Fixed the high vulnerability (CVE-2025-45141) in the tar library. The library was updated to version 7.5.11.
- Fixed the high vulnerability (CVE-2026-27148) in the storybook library. The library was updated to version 10.2.10.
- Fixed the high vulnerability (CVE-2026-27606) in the rollup library. The library was updated to version 4.59.0.
- Fixed the high vulnerability (CVE-2026-27904) in the minimatch library. The library was updated to version 3.1.3.
- Fixed the high vulnerability (CVE-2026-27959) in the koa library. The library was updated to version 2.16.4.
- Fixed the high vulnerability (CVE-2026-32141) in the flatted library. The library was updated to version 4.4.0.
### 26.0.0 2026-03-02
### Changes since 25.3.0:
@@ -46,6 +46,8 @@
class="io.cloudbeaver.websocket.event.client.WSSessionTaskConfirmationEvent"/>
<clientEvent id="cb_client_session_task_with_parameters_confirmation"
class="io.cloudbeaver.websocket.event.client.WSSessionTaskWithParametersConfirmationEvent"/>
<clientEvent id="cb_client_cancel_action"
class="io.cloudbeaver.websocket.event.client.WSSessionCancelActionClientEvent"/>
</extension>
</plugin>
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* Copyright (C) 2010-2026 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.
@@ -1037,7 +1037,6 @@ public class WebSession extends BaseWebSession
}
}
public void handleTaskConfirmationWithParameters(@NotNull String taskId, @NotNull Map<String, Object> parameters) {
String attributeName = getTaskConfirmationAttributeName(taskId);
CompletableFuture<Map<String, Object>> confirmationFuture = getAttribute(attributeName);
@@ -1049,6 +1048,16 @@ public class WebSession extends BaseWebSession
}
}
public void handleActionCancelledEvent(@NotNull String actionId) {
Object attributeValue = getAttribute(actionId);
if (!(attributeValue instanceof CompletableFuture<?> future)) {
log.info("Action to be cancelled not found, skipping: " + actionId);
return;
}
future.completeExceptionally(new DBWebException("Action cancelled by user: " + actionId));
removeAttribute(actionId);
}
@NotNull
private String getTaskConfirmationAttributeName(@NotNull String taskId) {
return WebSQLConstants.TASK_CONFIRMATION_ATTR_PREFIX + taskId;
@@ -193,8 +193,12 @@ public class ServletAppUtils {
@NotNull String origin
) throws DBException {
String finalOrigin = webAuthApplication.modifyOrigin(origin);
StringBuilder authUriBuilder = new StringBuilder(removeSideSlashes(finalOrigin));
String serviceUriSegment = removeSideSlashes(webAuthApplication.getAuthServiceUriSegment());
String rootUri = removeSideSlashes(getServletApplication().getRootURI());
if (finalOrigin.endsWith("/" + rootUri) && serviceUriSegment.startsWith(rootUri + "/")) {
finalOrigin = finalOrigin.substring(0, finalOrigin.length() - rootUri.length());
}
StringBuilder authUriBuilder = new StringBuilder(removeSideSlashes(finalOrigin));
if (CommonUtils.isNotEmpty(serviceUriSegment)) {
authUriBuilder.append("/").append(serviceUriSegment);
}
@@ -0,0 +1,40 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2026 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.websocket.event.client;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.WSClientEvent;
public class WSSessionCancelActionClientEvent extends WSClientEvent {
public static final String ID = "cb_client_cancel_action";
@NotNull
private final String actionId;
public WSSessionCancelActionClientEvent(
@NotNull String actionId
) {
super(ID, WSConstants.TOPIC_SESSION_ACTION);
this.actionId = actionId;
}
@NotNull
public String getActionId() {
return actionId;
}
}
@@ -14,6 +14,7 @@
<resource name="drivers/jaybird"/>
<resource name="drivers/h2"/>
<resource name="drivers/h2_v2"/>
<resource name="drivers/h2_v3"/>
<resource name="drivers/sqlite/xerial"/>
<resource name="drivers/mssql/new"/>
<resource name="drivers/trino"/>
@@ -35,6 +36,7 @@
<bundle id="drivers.firebird" label="Firebird drivers"/>
<bundle id="drivers.h2" label="H2 drivers"/>
<bundle id="drivers.h2_v2" label="H2 v2 drivers"/>
<bundle id="drivers.h2_v3" label="H2 v2.4+ drivers"/>
<bundle id="drivers.sqlite.xerial" label="SQLite drivers"/>
<bundle id="drivers.mssql.new" label="SQL Server drivers"/>
<bundle id="drivers.trino" label="Trino drivers"/>
@@ -55,6 +57,7 @@
<driver id="clickhouse:com_clickhouse"/>
<driver id="h2:h2_embedded"/>
<driver id="h2:h2_embedded_v2"/>
<driver id="h2:h2_embedded_v3"/>
<driver id="sqlite:sqlite_jdbc"/>
<driver id="sqlite:libsql_jdbc"/>
<driver id="sqlserver:microsoft"/>
@@ -379,6 +379,63 @@ public class CBSessionManager implements WebAppSessionManager {
}
}
@Nullable
public WebSession getWebSession(
@Nullable String smAccessToken,
@NotNull WebHttpRequestInfo requestInfo,
boolean create
) throws DBException {
if (CommonUtils.isEmpty(smAccessToken)) {
return null;
}
synchronized (sessionMap) {
var tempCredProvider = new SMTokenCredentialProvider(smAccessToken);
SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions();
var sessionId = requestInfo.getId() != null ? requestInfo.getId()
: authPermissions.getSessionId();
var existSession = sessionMap.get(sessionId);
if (existSession instanceof WebSession webSession) {
var creds = webSession.getUserContext().getActiveUserCredentials();
if (creds == null || !smAccessToken.equals(creds.getSmAccessToken())) {
if (webSession.getUserContext().refresh(
smAccessToken,
null,
authPermissions
)) {
webSession.refreshUserData();
}
}
return webSession;
}
if (existSession != null) {
//session exist but it not web session
return null;
}
if (!create) {
return null;
}
if (requestInfo.getId() == null) {
requestInfo = new WebHttpRequestInfo(
sessionId,
requestInfo.getLocale(),
requestInfo.getLastRemoteAddress(),
requestInfo.getLastRemoteUserAgent()
);
}
var webSession = createWebSessionImpl(requestInfo);
webSession.getUserContext().refresh(
smAccessToken,
null,
authPermissions
);
webSession.refreshUserData();
sessionMap.put(sessionId, webSession);
return webSession;
}
}
/**
* Send session state with remaining alive time to all cached session
*/
@@ -38,7 +38,8 @@ enum CBClientEventId {
cb_client_topic_subscribe,
cb_client_topic_unsubscribe,
cb_client_projects_active,
cb_client_session_ping
cb_client_session_ping,
cb_client_cancel_action @since(version: "26.0.2")
}
"Client subscribes on topic to receive only related events"
@@ -136,6 +137,13 @@ type CBProjectsActiveEvent implements CBClientEvent {
projectIds: [String!]!
}
"Action cancelled by user"
type CBActionCancelledEvent implements CBClientEvent @since(version: "26.0.2") {
id: CBClientEventId!
topicId: CBEventTopic
actionId: String!
}
"Async task info status event"
type WSAsyncTaskInfo @since(version: "24.3.1") {
id: CBServerEventId!
@@ -167,6 +175,7 @@ type WSOpenUrlEvent implements CBServerEvent @since(version: "25.3.1") {
topicId: CBEventTopic!
timestamp: Int!
url: String!
actionId: String!
}
extend type Query {
@@ -20,10 +20,7 @@ import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.utils.WebCommonUtils;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.DBConstants;
import org.jkiss.dbeaver.model.connection.DBPAuthModelDescriptor;
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.preferences.DBPPropertySource;
@@ -77,6 +74,7 @@ public class WebDatabaseAuthModel {
@Property
public WebPropertyInfo[] getProperties() throws DBWebException {
DBPPropertySource credentialsSource = model.createCredentialsSource(null, null);
Predicate<DBPPropertyDescriptor> predicate = CommonUtils.isEmpty(getRequiredAuth())
? p -> true
@@ -84,18 +82,9 @@ public class WebDatabaseAuthModel {
return Arrays.stream(credentialsSource.getProperties())
.filter(predicate)
.filter(p -> !isPassword(p) || isPasswordApplicable())
.filter(p -> !p.isDesktop())
.map(p -> new WebPropertyInfo(webSession, p, credentialsSource))
.toArray(WebPropertyInfo[]::new);
.map(p -> new WebPropertyInfo(webSession, p, credentialsSource)).toArray(WebPropertyInfo[]::new);
}
private boolean isPassword(@NotNull DBPPropertyDescriptor property) {
return property.hasFeature(DBConstants.PROP_FEATURE_PASSWORD);
}
private boolean isPasswordApplicable() {
return model.getInstance() instanceof AuthModelDatabaseNative<?> nativeModel &&
nativeModel.isUserPasswordApplicable();
}
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2026 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.
@@ -91,6 +91,13 @@ public class CBClientEventProcessor {
}
break;
}
case WSSessionCancelActionClientEvent.ID: {
if (webSession instanceof WebSession session) {
var cancelActionEvent = (WSSessionCancelActionClientEvent) clientEvent;
session.handleActionCancelledEvent(cancelActionEvent.getActionId());
}
break;
}
default:
var e = new DBWebException("Unknown client event: " + clientEvent.getId());
log.error(e.getMessage(), e);
@@ -24,7 +24,6 @@ import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.server.HandshakeRequest;
import jakarta.websocket.server.ServerEndpointConfig;
import org.eclipse.jetty.ee11.websocket.jakarta.server.internal.JakartaWebSocketCreator;
import org.eclipse.jetty.http.BadMessageException;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -89,7 +88,7 @@ public class CBWebSocketServerConfigurator extends ServerEndpointConfig.Configur
throw new RuntimeException(e.getMessage(), e);
}
if (sec.getUserProperties().get(PROP_WEB_SESSION) == null) {
throw new BadMessageException("No web session found for websocket request");
throw new RuntimeException("No web session found for websocket request");
}
}
@@ -97,8 +96,8 @@ public class CBWebSocketServerConfigurator extends ServerEndpointConfig.Configur
private String getSessionId(@NotNull HandshakeRequest request) {
// complex auth uses bearer authentication
List<String> authHeaders = WSClientUtils.getHeaders(request.getHeaders(), HttpConstants.HEADER_AUTHORIZATION);
if (!CommonUtils.isEmpty(authHeaders) && authHeaders.get(0).startsWith("Bearer ")) {
return authHeaders.get(0).substring(7);
if (!CommonUtils.isEmpty(authHeaders) && authHeaders.getFirst().startsWith(HttpConstants.BEARER_PREFIX)) {
return authHeaders.getFirst().substring(7);
}
return request.getHttpSession() instanceof HttpSession httpSession ? httpSession.getId() : null;
}
@@ -35,6 +35,7 @@ import org.jkiss.dbeaver.runtime.DBWorkbench;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Arrays;
@@ -213,6 +214,8 @@ public class WebServiceFS implements DBWServiceFS {
oldParentNode.removeChildResource(oldNode.getPath());
parentNode.addChildResource(to);
return new FSFile(parentNode.getChild(to));
} catch (NoSuchFileException e) {
throw new DBWebException("File not found. Please refresh the catalog and check if file exists.");
} catch (Exception e) {
throw new DBWebException("Failed to move file: " + e.getMessage(), e);
}
@@ -32,7 +32,6 @@ import io.cloudbeaver.service.security.bruteforce.UserLoginRecord;
import io.cloudbeaver.service.security.db.CBDatabase;
import io.cloudbeaver.service.security.internal.AuthAttemptSessionInfo;
import io.cloudbeaver.service.security.internal.CBAuthSubjectRepo;
import io.cloudbeaver.service.security.internal.SMTokenInfo;
import io.cloudbeaver.utils.WebEventUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -2437,7 +2436,9 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
}
}
private SMTokenInfo readAccessTokenInfo(String smAccessToken) throws DBException {
@NotNull
@Override
public SMTokenInfo readAccessTokenInfo(@NotNull String smAccessToken) throws DBException {
try (Connection dbCon = database.openConnection();
PreparedStatement dbStat = dbCon.prepareStatement(
"""
@@ -1,84 +0,0 @@
/*
* 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.service.security.internal;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
public class SMTokenInfo {
@NotNull
private final String accessToken;
@NotNull
private final String refreshToken;
@NotNull
private final String sessionId;
@NotNull
private final String userId;
@Nullable
private final String authRole;
private final boolean serviceToken;
public SMTokenInfo(
@NotNull String accessToken,
@NotNull String refreshToken,
@NotNull String sessionId,
@NotNull String userId,
@Nullable String authRole,
boolean serviceToken
) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.sessionId = sessionId;
this.userId = userId;
this.authRole = authRole;
this.serviceToken = serviceToken;
}
@NotNull
public String getRefreshToken() {
return refreshToken;
}
@NotNull
public String getSessionId() {
return sessionId;
}
@NotNull
public String getUserId() {
return userId;
}
@NotNull
public String getAccessToken() {
return accessToken;
}
@Nullable
public String getAuthRole() {
return authRole;
}
public boolean isServiceToken() {
return serviceToken;
}
}
+27
View File
@@ -0,0 +1,27 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>drivers.h2_v3</artifactId>
<version>1.0.0</version>
<parent>
<groupId>io.cloudbeaver</groupId>
<artifactId>drivers</artifactId>
<version>1.0.0</version>
<relativePath>../</relativePath>
</parent>
<packaging>pom</packaging>
<properties>
<deps.output.dir>h2_v3</deps.output.dir>
</properties>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
+1
View File
@@ -23,6 +23,7 @@
<module>duckdb</module>
<module>h2</module>
<module>h2_v2</module>
<module>h2_v3</module>
<module>jaybird</module>
<module>kyuubi</module>
<module>mysql</module>
+1 -1
View File
@@ -19,7 +19,7 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>13.2.1.jre11</version>
<version>13.4.0.jre11</version>
</dependency>
<!--
<dependency>
@@ -179,6 +179,20 @@ public class SQLQueryTranslatorTest extends CloudbeaverMockTest {
}
@Test
public void createTableWithAscii() throws DBException {
var basicSql = "CREATE TABLE CB_TEST_TYPES (ASCII_COLUMN VARCHAR(128) ASCII NOT NULL);\n";
Map<SQLDialect, String> expectedSqlByDialect = new HashMap<>();
expectedSqlByDialect.put(new H2SQLDialect(), "CREATE TABLE CB_TEST_TYPES (ASCII_COLUMN VARCHAR (128) NOT NULL);\n");
expectedSqlByDialect.put(new PostgreDialect(), "CREATE TABLE CB_TEST_TYPES (ASCII_COLUMN VARCHAR (128) NOT NULL);\n");
expectedSqlByDialect.put(
new MySQLDialect(),
"CREATE TABLE CB_TEST_TYPES (ASCII_COLUMN VARCHAR (128) CHARACTER SET latin1 NOT NULL);"
);
translateAndValidateQueries(basicSql, expectedSqlByDialect);
}
private static void translateAndValidateQueries(
@NotNull String basicSql,
@NotNull Map<SQLDialect, String> expectedSqlByDialect
@@ -203,7 +217,10 @@ public class SQLQueryTranslatorTest extends CloudbeaverMockTest {
private static String normalizeScript(String script) {
// Unify for tests
return script.toLowerCase().replace(GeneralUtils.getDefaultLineSeparator(), "\n");
return script.toLowerCase()
.replaceAll("\\s", " ")
.replace(GeneralUtils.getDefaultLineSeparator(), "\n")
.trim();
}
}
@@ -12,9 +12,6 @@
.rdg-search-panel {
padding-bottom: 0 !important;
.search-panel__inputs {
max-width: var(--tw-container-lg);
}
}
.rdg-cell-search-match {
@@ -1,6 +1,6 @@
/*
* 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.
@@ -9,11 +9,31 @@
import { createContext } from 'react';
import type { LocalizationToken } from './LocalizationToken.js';
export function defaultTranslateFn<T extends LocalizationToken | undefined>(token: T, fallback?: T): T {
return fallback || token;
export function interpolateTranslation(template: string, args?: Record<string | number, unknown>): string {
if (!args) {
return template;
}
let result = template;
for (const [key, value] of Object.entries(args)) {
if (value != null) {
result = result.split(`{arg:${key}}`).join(String(value));
}
}
return result;
}
export type TranslateFn = <T extends LocalizationToken | undefined>(token: T, fallback?: T, args?: Record<string | number, any>) => T;
export function defaultTranslateFn<T extends LocalizationToken | undefined>(token: T, fallback?: T, args?: Record<string | number, unknown>): T {
const result = fallback || token;
if (result && args) {
return interpolateTranslation(result as string, args) as T;
}
return result;
}
export type TranslateFn = <T extends LocalizationToken | undefined>(token: T, fallback?: T, args?: Record<string | number, unknown>) => T;
export const TranslateContext = createContext<{ translate: TranslateFn }>({
translate: (key, fallback) => fallback || key,
translate: defaultTranslateFn,
});
@@ -1,11 +1,12 @@
/*
* 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 clsx from 'clsx';
import { Button as AriaButton, type ButtonProps as UnstyledButtonProps } from '@ariakit/react';
import type { ControlSize } from '../types/controls.js';
import { componentProviderWrapper } from '../componentProviderWrapper.js';
import { ButtonRoot, type ButtonRootProps } from './ButtonRoot.js';
@@ -37,3 +38,6 @@ export function ButtonBaseIcon({ className, children, placement }: ButtonIconPro
export const Button = componentProviderWrapper('Button', ButtonBase);
export const ButtonIcon = componentProviderWrapper('ButtonIcon', ButtonBaseIcon);
export const UnstyledButton = AriaButton;
export { type UnstyledButtonProps };
@@ -1,7 +1,7 @@
@layer base {
:root {
--dbv-kit-btn-height: var(--dbv-kit-control-height-medium);
--dbv-kit-btn-padding-inline: calc(var(--tw-spacing) * 2.5);
--dbv-kit-btn-padding-inline: calc(var(--tw-spacing) * 3);
--dbv-kit-btn-gap: calc(var(--dbv-kit-btn-padding-inline) / 2);
--dbv-kit-btn-outline-offset: var(--dbv-kit-control-outline-offset);
@@ -0,0 +1,32 @@
/*
* 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 {
Composite as AriakitComposite,
CompositeItem as AriakitCompositeItem,
CompositeProvider as AriakitCompositeProvider,
useCompositeStore,
type CompositeProps,
type CompositeItemProps,
type CompositeProviderProps,
type CompositeStore,
} from '@ariakit/react';
export function CompositeProvider({ children, ...props }: CompositeProviderProps) {
return <AriakitCompositeProvider {...props}>{children}</AriakitCompositeProvider>;
}
export function Composite({ children, ...props }: CompositeProps) {
return <AriakitComposite {...props}>{children}</AriakitComposite>;
}
export function CompositeItem({ children, ...props }: CompositeItemProps) {
return <AriakitCompositeItem {...props}>{children}</AriakitCompositeItem>;
}
export { useCompositeStore, type CompositeProps, type CompositeItemProps, type CompositeProviderProps, type CompositeStore };
@@ -107,17 +107,18 @@
&[data-variant='slide'] {
position: absolute;
top: 0;
left: 0;
left: auto;
right: 0;
bottom: 0;
transform: translateX(100%);
width: 100%;
width: calc(100% - var(--dbv-kit-dialog-slide-inset));
max-width: none;
height: 100%;
max-height: 100%;
min-width: 0;
min-height: 0;
border-radius: 0;
will-change: transform;
@media (prefers-reduced-motion: no-preference) {
opacity: 1;
@@ -24,8 +24,9 @@
--dbv-kit-dialog-max-width: 720px;
/* Slide variant (side panel) */
--dbv-kit-dialog-slide-transition-duration: 300ms;
--dbv-kit-dialog-slide-transition-timing: ease-in-out;
--dbv-kit-dialog-slide-inset: 120px;
--dbv-kit-dialog-slide-transition-duration: 350ms;
--dbv-kit-dialog-slide-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
/* Backdrop */
--dbv-kit-dialog-backdrop-background: #0000007a;
@@ -45,6 +45,8 @@ import {
type MenuListProps,
type MenuBarProps,
type MenuItemCheckProps,
type MenuStore,
type MenuStoreState
} from '@ariakit/react';
import clsx from 'clsx';
@@ -175,6 +177,8 @@ export {
type MenuStoreProps,
type MenuArrowProps,
type HovercardStoreState,
type MenuStore,
type MenuStoreState
};
Menu.Provider = MenuProvider;
@@ -1,6 +1,6 @@
/*
* 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.
@@ -11,6 +11,10 @@ import clsx from 'clsx';
import './Popover.css';
export function PopoverAnchor({ children, ...props }: Ariakit.PopoverAnchorProps) {
return <Ariakit.PopoverAnchor {...props}>{children}</Ariakit.PopoverAnchor>;
}
export function Popover({ children, ...props }: Ariakit.PopoverProviderProps) {
return <Ariakit.PopoverProvider {...props}>{children}</Ariakit.PopoverProvider>;
}
@@ -40,3 +44,6 @@ Popover.PopoverArrow = PopoverArrow;
Popover.PopoverHeading = PopoverHeading;
Popover.PopoverDescription = PopoverDescription;
Popover.PopoverContent = PopoverContent;
Popover.Anchor = PopoverAnchor;
export { usePopoverStore, type PopoverStore } from '@ariakit/react';
@@ -0,0 +1,15 @@
/*
* 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 { Portal as AriakitPortal, type PortalProps } from '@ariakit/react';
export function Portal({ children, ...props }: PortalProps) {
return <AriakitPortal {...props}>{children}</AriakitPortal>;
}
export type { PortalProps };
@@ -52,6 +52,7 @@
flex-direction: column;
flex: 1 1 420px;
min-width: 120px;
max-width: var(--tw-container-lg);
gap: calc(var(--tw-spacing) / 2);
}
@@ -21,7 +21,7 @@ export {
type FocusTrapRegionProps,
} from '@ariakit/react';
export { Button, ButtonBase, type ButtonProps, ButtonIcon, type ButtonIconProps } from './Button/Button.js';
export { Button, ButtonBase, type ButtonProps, ButtonIcon, type ButtonIconProps, UnstyledButton, type UnstyledButtonProps } from './Button/Button.js';
export { IconButton, IconButtonBase, type IconButtonProps } from './IconButton/IconButton.js';
export { Checkbox, CheckboxBase, type CheckboxProps } from './Checkbox/Checkbox.js';
export { ColorPicker } from './ColorPicker/ColorPicker.js';
@@ -46,7 +46,7 @@ export {
} from './Select/Select.js';
export * from './Combobox/Combobox.js';
export * from './Command/Command.js';
export { Popover } from './Popover/Popover.js';
export { Popover, usePopoverStore, type PopoverStore } from './Popover/Popover.js';
export { SelectField, type SelectFieldProps, type SelectItem } from './Select/SelectField.js';
export { Spinner, type SpinnerProps } from './Spinner/Spinner.js';
export { Radio, RadioGroup, useRadioContext, useRadioStore, type RadioProviderProps, type RadioProps, type RadioGroupProps } from './Radio/index.js';
@@ -57,3 +57,14 @@ export * from './Menu/Menu.js';
export * from './Disclosure/Disclosure.js';
export * from './Dialog/Dialog.js';
export * from './Tab/Tab.js';
export { Portal, type PortalProps } from './Portal/Portal.js';
export {
Composite,
CompositeItem,
CompositeProvider,
useCompositeStore,
type CompositeProps,
type CompositeItemProps,
type CompositeProviderProps,
type CompositeStore,
} from './Composite/Composite.js';
+3
View File
@@ -11,6 +11,9 @@
"clear": "rimraf -g '@dbeaver/*/{dist,lib}'",
"test": "dbeaver-test"
},
"resolutions": {
"happy-dom": ">=20.8.8"
},
"devDependencies": {
"@dbeaver/cli": "workspace:^",
"@dbeaver/prettier-config": "workspace:^",
@@ -0,0 +1,24 @@
/*
* 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.
*/
export function formatNumber(n: number, d: number): string {
if (n < 1000) {
return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
const numStr = n.toString();
const exponent = numStr.length - (numStr.length % 3);
const power = Math.pow(10, d);
const rounded = Math.round((n * power) / Math.pow(10, exponent)) / power;
const units = ' kMBTPE';
const unit = units[exponent / 3]!;
return rounded + unit;
}
@@ -16,3 +16,4 @@ export * from './memoizeLast.js';
export * from './mutex.js';
export * from './reorderArray.js';
export * from './getLocalizedDisplayName.js';
export * from './formatNumber.js';
+3 -2
View File
@@ -24,7 +24,9 @@
"koa": ">=2.16.4",
"immutable": ">=4.3.8",
"serialize-javascript": ">=7.0.3",
"flatted": ">=3.4.0"
"flatted": ">=3.4.2",
"picomatch": ">=2.3.2",
"happy-dom": ">=20.8.8"
},
"scripts": {
"test": "dbeaver-test",
@@ -59,7 +61,6 @@
"prettier": "^3.6.2",
"react": "^19",
"react-dom": "^19",
"reakit": "^1",
"rimraf": "^6",
"typescript": "^5.8.3",
"typescript-plugin-css-modules": "^5",
@@ -1,16 +1,14 @@
/*
* 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.
*/
.container {
flex: 1;
position: relative;
overflow: clip;
min-width: 0;
}
.loader {
height: 100%;
}
.slideBox {
flex: 1;
}
.pane {
display: flex;
composes: theme-background-surface theme-text-on-surface from global;
@@ -1,26 +1,13 @@
/*
* 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 {
Loader,
Pane,
Placeholder,
ResizerControls,
s,
SlideBox,
SlideElement,
SlideOverlay,
Split,
useS,
useSplitUserState,
SlidePanel,
} from '@cloudbeaver/core-blocks';
import { Loader, Pane, Placeholder, ResizerControls, s, SlideDialog, Split, useS, useSplitUserState } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
@@ -46,26 +33,23 @@ export const RightArea = observer<Props>(function RightArea({ className }) {
}
return (
<SlideBox open={optionsPanelService.active} className={s(styles, { slideBox: true }, className)} onClose={close}>
<SlideElement inert={optionsPanelService.active}>
<Split {...splitState} sticky={30} split="horizontal" mode={toolsDisabled ? 'minimize' : splitState.mode} disable={toolsDisabled} keepRatio>
<Pane className={s(styles, { pane: true })}>
<Loader className={s(styles, { loader: true })} suspense>
<Placeholder container={appScreenService.rightAreaTop} />
</Loader>
</Pane>
<ResizerControls />
<Pane className={s(styles, { pane: true })} basis="30%" main>
<Loader className={s(styles, { loader: true })} suspense>
<Placeholder container={appScreenService.rightAreaBottom} />
</Loader>
</Pane>
</Split>
</SlideElement>
<SlideOverlay onClick={close} />
<SlidePanel isOpen={optionsPanelService.active} onClose={close}>
<div className={s(styles, { container: true }, className)}>
<Split {...splitState} sticky={30} split="horizontal" mode={toolsDisabled ? 'minimize' : splitState.mode} disable={toolsDisabled} keepRatio>
<Pane className={s(styles, { pane: true })}>
<Loader className={s(styles, { loader: true })} suspense>
<Placeholder container={appScreenService.rightAreaTop} />
</Loader>
</Pane>
<ResizerControls />
<Pane className={s(styles, { pane: true })} basis="30%" main>
<Loader className={s(styles, { loader: true })} suspense>
<Placeholder container={appScreenService.rightAreaBottom} />
</Loader>
</Pane>
</Split>
<SlideDialog open={optionsPanelService.active} onClose={close}>
<OptionsPanel />
</SlidePanel>
</SlideBox>
</SlideDialog>
</div>
);
});
-2
View File
@@ -44,8 +44,6 @@
"react-dom": "^19",
"react-hotkeys-hook": "5.1.0",
"react-minisearch": "^7",
"reakit": "^1",
"reakit-utils": "^0",
"subscript": "^9",
"tslib": "^2"
},
+2 -3
View File
@@ -7,7 +7,6 @@
*/
@layer components {
.dbv-kit-button {
--dbv-kit-btn-padding-inline: 15px;
--dbv-kit-btn-outline-color: var(--theme-primary);
--dbv-kit-control-disabled-foreground: #3535355e;
--dbv-kit-control-disabled-background: #3535351f;
@@ -22,7 +21,7 @@
--dbv-kit-btn-primary-border-color: var(--theme-primary);
--dbv-kit-btn-secondary-background: transparent;
--dbv-kit-btn-secondary-background-hover: color-mix(in srgb, transparent, var(--theme-primary) 5%);
--dbv-kit-btn-secondary-background-hover: color-mix(in srgb, transparent, var(--theme-primary) 10%);
--dbv-kit-btn-secondary-background-active: color-mix(in srgb, transparent, var(--theme-primary) 20%);
--dbv-kit-btn-secondary-foreground: var(--theme-link-color);
--dbv-kit-btn-secondary-foreground-hover: var(--theme-link-color);
@@ -37,7 +36,7 @@
--dbv-kit-btn-ghost-foreground-hover: var(--theme-link-color);
--dbv-kit-btn-ghost-foreground-active: var(--theme-link-color);
--dbv-kit-btn-ghost-background: transparent;
--dbv-kit-btn-ghost-background-hover: color-mix(in srgb, transparent, var(--theme-primary) 5%);
--dbv-kit-btn-ghost-background-hover: color-mix(in srgb, transparent, var(--theme-primary) 10%);
--dbv-kit-btn-ghost-background-active: color-mix(in srgb, transparent, var(--theme-primary) 20%);
--dbv-kit-btn-ghost-border-color: transparent;
@@ -1,82 +1,45 @@
/*
* 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 type { RefObject } from 'react';
import BaseDropdownStyles from '../FormControls/BaseDropdown.module.css';
import { IconOrImage } from '../IconOrImage.js';
import { Menu } from '../Menu/Menu.js';
import { MenuItem } from '../Menu/MenuItem.js';
import type { IMenuState } from '../Menu/MenuStateContext.js';
import { s } from '../s.js';
import { Text } from '../Text.js';
import { useS } from '../useS.js';
import style from './InputAutocompletionMenu.module.css';
import { observer } from 'mobx-react-lite';
import { Composite, CompositeItem, Popover } from '@dbeaver/ui-kit';
import { type InputAutocompleteProposal } from './useInputAutocomplete.js';
import { useS } from '../useS.js';
import { BaseDropdownStyles, IconOrImage, s, Text } from '../index.js';
import { forwardRef } from 'react';
interface AutocompletionProps {
position: { x: number; y: number };
proposals: InputAutocompleteProposal[];
menuRef: RefObject<IMenuState>;
className?: string;
onSelect?: (proposal: InputAutocompleteProposal) => void;
}
export const InputAutocompletionMenu = observer(function InputAutocompletionMenu({
position,
className,
menuRef,
export const InputAutocompletionMenu = observer(forwardRef<HTMLDivElement, AutocompletionProps>(function InputAutocompletionMenu({
proposals,
onSelect,
}: AutocompletionProps) {
const styles = useS(style, BaseDropdownStyles);
const contextMenuPosition = {
position,
open: () => {},
close: () => {},
};
}, ref) {
const styles = useS(BaseDropdownStyles);
function handleSelect(proposal: InputAutocompleteProposal) {
menuRef.current?.hide();
onSelect?.(proposal);
}
if (!proposals.length) {
return;
}
// TODO: migrate to new menu or popover
return (
<Menu
contextMenuPosition={contextMenuPosition}
visible={proposals.length > 0}
panelAvailable={proposals.length > 0}
className={s(styles, { menu: true }, className)}
menuRef={menuRef}
label="Autocompletion"
items={proposals.map(proposal => (
<MenuItem
key={proposal.displayString}
id={proposal.displayString}
type="button"
title={proposal.title}
className={s(styles, { menuItem: true })}
onClick={event => handleSelect(proposal)}
>
{proposal.icon && (
<div className={s(styles, { itemIcon: true })}>
<IconOrImage icon={proposal.icon} className={s(styles, { iconOrImage: true })} />
</div>
)}
<Text truncate>{proposal.displayString}</Text>
</MenuItem>
))}
modal
/>
<Popover.PopoverContent ref={ref} className={s(styles, { menu: true })} gutter={4} modal={false} autoFocusOnShow={false} unmountOnHide>
<Composite>
{proposals.map(proposal => (
<CompositeItem key={proposal.title} className={s(styles, { menuItem: true }, 'tw:w-full')} onClick={() => onSelect?.(proposal)}>
{proposal.icon && (
<div className={s(styles, { itemIcon: true })}>
<IconOrImage icon={proposal.icon} className={s(styles, { iconOrImage: true })} />
</div>
)}
<Text truncate>{proposal.displayString}</Text>
</CompositeItem>
))}
</Composite>
</Popover.PopoverContent>
);
});
}));
@@ -18,12 +18,3 @@
align-items: center;
gap: 2px;
}
.iconButton {
width: 12px;
height: 12px;
flex-shrink: 0;
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
@@ -9,10 +9,10 @@ import { observer } from 'mobx-react-lite';
import { type ReactNode, useContext, useState } from 'react';
import { blobToBase64, bytesToSize } from '@cloudbeaver/core-utils';
import { IconButton } from '@dbeaver/ui-kit';
import { Button } from '../Button.js';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps.js';
import { IconButton } from '../IconButton.js';
import { useTranslate } from '../localization/useTranslate.js';
import { s } from '../s.js';
import { UploadArea } from '../UploadArea.js';
@@ -22,6 +22,7 @@ import { FieldDescription } from './FieldDescription.js';
import { FieldLabel } from './FieldLabel.js';
import { FormContext } from './FormContext.js';
import inputFileTextContentStyles from './InputFileTextContent.module.css';
import { Icon } from '../Icon.js';
const DEFAULT_MAX_FILE_SIZE = 2048;
@@ -139,13 +140,24 @@ export const InputFileTextContent: InputFileTextContentType = observer(function
{children}
</FieldLabel>
<UploadArea title={tooltip} disabled={disabled} accept={accept} reset onChange={handleChange}>
<Button className="tw:inline-flex" icon="/icons/import.svg" tag="div" variant="secondary" disabled={disabled}>
<Button className="tw:w-max" icon="/icons/import.svg" tag="div" variant="secondary" disabled={disabled}>
{translate('ui_upload_file')}
</Button>
</UploadArea>
<FieldDescription className={s(styles, { fieldDescription: true })}>
{description}
{(selected || saved) && <IconButton className={s(styles, { iconButton: true })} disabled={disabled} name="cross" onClick={removeFile} />}
{(selected || saved) && (
<IconButton
className="tw:shrink-0"
variant="secondary"
disabled={disabled}
size="small"
aria-label={translate('ui_remove')}
onClick={removeFile}
>
<Icon name="cross" width={12} height={12} />
</IconButton>
)}
</FieldDescription>
</Field>
);
@@ -184,7 +184,7 @@ export const InputFiles: InputFilesType = observer(
<UploadArea ref={ref} {...rest} name={name} value={value} required={required} reset onChange={handleChange}>
<Button
iconPlacement="start"
className="tw:inline-flex tw:justify-center"
className="tw:w-max tw:justify-center"
icon="/icons/import.svg"
tag="div"
loading={loading}
@@ -143,7 +143,7 @@ export const Textarea: TextareaType = observer(function Textarea({
}
}}
>
<Button className="tw:inline-flex" tag="div" disabled={rest.disabled || rest.readOnly} variant="secondary">
<Button className="tw:w-max" tag="div" disabled={rest.disabled || rest.readOnly} variant="secondary">
{translate('ui_file')}
</Button>
</UploadArea>
@@ -1,22 +1,22 @@
/*
* 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 { useContext, useEffect, useRef } from 'react';
import { ExecutorInterrupter } from '@cloudbeaver/core-executor';
import { useEffect, useRef } from 'react';
import { useTranslate } from '../localization/useTranslate.js';
import { useExecutor } from '../useExecutor.js';
import { FormContext } from './FormContext.js';
export interface ICustomInputValidation<TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement> {
ref: React.RefObject<TType | null>;
revalidate: () => void;
revalidateAndReport: (element: TType) => boolean;
}
export function useCustomInputValidation<T = void, TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(
validation: (value: T) => string | null,
): React.RefObject<TType | null> {
const context = useContext(FormContext);
): ICustomInputValidation<TType> {
const inputRef = useRef<TType | null>(null);
const translate = useTranslate();
@@ -27,37 +27,32 @@ export function useCustomInputValidation<T = void, TType extends HTMLInputElemen
value = element.value as unknown as T;
}
const valid = element.validity.valid;
const result = validation(value);
try {
if (typeof result === 'string') {
element.setCustomValidity(result || translate('core_blocks_custom_input_validation_error'));
return false;
}
element.setCustomValidity('');
return true;
} finally {
if (valid !== element.validity.valid) {
element.reportValidity();
}
if (typeof result === 'string') {
element.setCustomValidity(result || translate('core_blocks_custom_input_validation_error'));
return false;
}
element.setCustomValidity('');
return true;
}
useExecutor({
executor: context?.onValidate,
handlers: [
function validationHandler(_, context) {
if (!inputRef.current) {
return;
}
function revalidateAndReport(element: TType): boolean {
const valid = element.validity.valid;
const result = validate(element);
if (!validate(inputRef.current)) {
ExecutorInterrupter.interrupt(context);
}
},
],
});
if (valid !== element.validity.valid) {
element.reportValidity();
}
return result;
}
function revalidate() {
if (inputRef.current) {
validate(inputRef.current);
}
}
useEffect(() => {
const element = inputRef.current;
@@ -68,14 +63,14 @@ export function useCustomInputValidation<T = void, TType extends HTMLInputElemen
function handleInput(event: Event) {
const target = event.target as TType;
if (target.validity.valid === false) {
validate(target);
revalidateAndReport(target);
}
}
function handleBlur(event: Event) {
const target = event.target as TType;
if (target.validity.valid === true) {
validate(target);
revalidateAndReport(target);
}
}
@@ -88,5 +83,5 @@ export function useCustomInputValidation<T = void, TType extends HTMLInputElemen
};
});
return inputRef;
return { ref: inputRef, revalidate, revalidateAndReport };
}
@@ -1,6 +1,6 @@
/*
* 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.
@@ -0,0 +1,42 @@
/*
* 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 { useContext } from 'react';
import { FormContext, useExecutor, type IFormContext } from '../index.js';
import { useCustomInputValidation, type ICustomInputValidation } from './useCustomInputValidation.js';
import { ExecutorInterrupter } from '@cloudbeaver/core-executor';
export function useFormCustomInputValidation<T = void, TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(
validation: (value: T) => string | null,
formContext?: IFormContext,
): ICustomInputValidation<TType> {
const customValidation = useCustomInputValidation<T, TType>(validation);
const reactContext = useContext(FormContext);
const context = formContext ?? reactContext;
if (!context) {
throw new Error('useCustomInputValidation must be used within a FormContext provider');
}
useExecutor({
executor: context?.onValidate,
handlers: [
function validationHandler(_, context) {
if (!customValidation.ref.current) {
return;
}
if (!customValidation.revalidateAndReport(customValidation.ref.current)) {
ExecutorInterrupter.interrupt(context);
}
},
],
});
return customValidation;
}
@@ -1,17 +1,17 @@
/*
* 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 { action, computed, observable } from 'mobx';
import { type RefObject, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import { type RefObject, useEffect, useLayoutEffect, useMemo } from 'react';
import { debounce } from '@cloudbeaver/core-utils';
import { isNotNullDefined } from '@dbeaver/js-helpers';
import type { IMenuState } from '../Menu/MenuStateContext.js';
import type { IContextMenuPositionCoords } from '../Menu/useContextMenuPosition.js';
import { useObservableRef } from '../useObservableRef.js';
import { type SearchStrategy, useSearch } from '../useSearch.js';
@@ -23,6 +23,7 @@ interface InputAutocompleteOptions {
separator?: string | RegExp;
matchStrategy?: SearchStrategy;
predicate?: (suggestion: InputAutocompleteProposal, lastWord?: string) => boolean;
onNavigate?: () => void;
}
export interface InputAutocompleteProposal {
@@ -38,7 +39,6 @@ interface State {
proposals: InputAutocompleteProposal[];
position: IContextMenuPositionCoords;
inputValue: string;
menuRef: RefObject<IMenuState>;
}
const DEFAULT_SEPARATOR = ' ';
@@ -48,7 +48,7 @@ const CONTEXT_INPUT_OFFSET_Y = 3;
export const useInputAutocomplete = (
inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement | null>,
{ sourceHints, separator = DEFAULT_SEPARATOR, matchStrategy = 'contains', predicate }: InputAutocompleteOptions,
{ sourceHints, separator = DEFAULT_SEPARATOR, matchStrategy = 'contains', onNavigate, predicate }: InputAutocompleteOptions,
): Readonly<State> => {
const search = useSearch({
sourceHints,
@@ -56,7 +56,7 @@ export const useInputAutocomplete = (
matchStrategy,
predicate,
});
const menuRef = useRef<IMenuState>(null);
const state = useObservableRef(
() => ({
position: { x: 0, y: 0 } as IContextMenuPositionCoords,
@@ -126,7 +126,7 @@ export const useInputAutocomplete = (
position: observable.ref,
inputValue: observable.ref,
},
{ inputRef, search, menuRef },
{ inputRef, search, onNavigate },
);
const handleInput = useMemo(
@@ -145,12 +145,11 @@ export const useInputAutocomplete = (
function handleKeyDown(event: any) {
switch (event.key) {
case 'Escape':
state.menuRef.current?.hide();
state.resetState();
break;
case 'ArrowDown':
case 'ArrowUp':
state.menuRef.current?.first();
state.onNavigate?.();
break;
case 'Tab':
state.resetState();
@@ -1,13 +1,14 @@
/*
* 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 type React from 'react';
import { type ButtonProps, Button as ReakitButton } from 'reakit';
import { UnstyledButton, type UnstyledButtonProps } from '@dbeaver/ui-kit';
import { Icon } from './Icon.js';
import IconButtonStyles from './IconButton.module.css';
@@ -16,25 +17,22 @@ import { StaticImage } from './StaticImage.js';
import { useS } from './useS.js';
interface Props {
tag?: 'button' | 'a' | 'div';
name: string;
img?: boolean;
viewBox?: string;
}
export type IconButtonProps = Props & ButtonProps;
export type IconButtonProps = Props & UnstyledButtonProps;
export const IconButton: React.FC<IconButtonProps> = observer(function IconButton({ tag, name, img, viewBox, className, ...rest }) {
export const IconButton: React.FC<IconButtonProps> = observer(function IconButton({ name, img, viewBox, className, ...rest }) {
const styles = useS(IconButtonStyles);
const Button = tag ?? ReakitButton;
return (
<Button tabIndex={0} {...rest} className={s(styles, { iconButton: true }, className)}>
<UnstyledButton tabIndex={0} {...rest} className={s(styles, { iconButton: true }, className)}>
<div className={s(styles, { iconBox: true })}>
{img && <StaticImage className={s(styles, { staticImage: true })} icon={name} />}
{!img && <Icon className={s(styles, { icon: true })} name={name} viewBox={viewBox} />}
</div>
</Button>
</UnstyledButton>
);
});
@@ -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.
*/
.menuButton {
composes: theme-ripple from global;
}
.menuButton {
background: none;
border: none;
color: inherit;
cursor: pointer;
& .box {
display: flex;
align-items: center;
flex: 1;
height: inherit;
}
}
.menuButtonLink {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
box-sizing: border-box;
padding: 0;
margin: 0;
border: none;
background: transparent;
}
@@ -1,202 +0,0 @@
/*
* 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 React, { forwardRef, useLayoutEffect, useRef, useState } from 'react';
import { MenuButton, type MenuInitialState, useMenuState } from 'reakit';
import type { ExtractHTMLAttributes } from 'reakit-utils';
import { ErrorBoundary } from '../ErrorBoundary.js';
import { s } from '../s.js';
import { useCombinedRef } from '../useCombinedRef.js';
import { useObjectRef } from '../useObjectRef.js';
import { useS } from '../useS.js';
import style from './Menu.module.css';
import { MenuPanel } from './MenuPanel.js';
import { type IMenuState, MenuStateContext } from './MenuStateContext.js';
import type { IContextMenuPosition } from './useContextMenuPosition.js';
interface IMenuProps extends React.ButtonHTMLAttributes<any> {
contextMenuPosition?: IContextMenuPosition;
label: string;
items: React.ReactNode | (() => React.ReactNode);
menuRef?: React.RefObject<IMenuState | null>;
disclosure?: boolean;
placement?: MenuInitialState['placement'];
submenu?: boolean;
modal?: boolean;
visible?: boolean;
rtl?: boolean;
hasBindings?: boolean;
panelAvailable?: boolean;
getHasBindings?: () => boolean;
onVisibleSwitch?: (visible: boolean) => void;
}
/**
* @deprecated use the @dbeaver/ui-kit package instead
*/
export const Menu = observer<IMenuProps, HTMLButtonElement>(
forwardRef(function Menu(
{
contextMenuPosition,
label,
items,
menuRef,
disclosure,
children,
placement,
visible,
hasBindings,
panelAvailable,
getHasBindings,
onVisibleSwitch,
modal,
submenu,
rtl,
className,
...props
},
ref,
) {
const innerMenuButtonRef = useRef<HTMLButtonElement>(null);
const combinedRef = useCombinedRef(ref, innerMenuButtonRef);
const [relativePosition, setRelativePosition] = useState<{ x: number; y: number } | null>(null);
const menuButtonLinkRef = useRef<HTMLButtonElement>(null);
const menuPanelRef = useRef<HTMLDivElement>(null);
const propsRef = useObjectRef({ onVisibleSwitch, visible });
const menu = useMenuState({
modal,
placement,
visible,
rtl,
unstable_fixed: true,
});
const styles = useS(style);
if (menuRef) {
menuRef.current = menu;
}
let menuVisible = menu.visible;
if (panelAvailable === false) {
menuVisible = false;
}
useLayoutEffect(() => {
if (!menuVisible) {
setRelativePosition(null);
}
propsRef.onVisibleSwitch?.(menuVisible);
}, [menuVisible]);
useLayoutEffect(() => {
if (!contextMenuPosition?.position) {
return;
}
if (menuVisible) {
menu.hide();
return;
}
if (innerMenuButtonRef.current) {
menu.show();
const boxSize = innerMenuButtonRef.current.getBoundingClientRect();
setRelativePosition({
x: contextMenuPosition.position.x - boxSize.x,
y: contextMenuPosition.position.y - boxSize.y,
});
contextMenuPosition.position = null;
}
}, [contextMenuPosition?.position, menuVisible]);
useLayoutEffect(() => {
if (relativePosition) {
if (menuButtonLinkRef.current) {
menuButtonLinkRef.current.style.left = `${relativePosition.x}px`;
menuButtonLinkRef.current.style.top = `${relativePosition.y}px`;
}
}
});
const MenuButtonLink = MenuButton;
if (React.isValidElement(children) && disclosure) {
return (
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton
key={relativePosition ? 'link' : 'main'}
ref={combinedRef}
tabIndex={0}
className={s(styles, { menuButton: true }, className)}
{...menu}
visible={menuVisible}
{...props}
{...(children.props as any)}
>
{(disclosureProps: ExtractHTMLAttributes<any>) => React.cloneElement(children, { ...disclosureProps, ...(children.props as any) })}
</MenuButton>
<MenuPanel
ref={menuPanelRef}
label={label}
menu={menu}
rtl={rtl}
submenu={submenu}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
>
{items}
</MenuPanel>
{relativePosition && (
<MenuButtonLink ref={menuButtonLinkRef} className={s(styles, { menuButtonLink: true })} {...menu} visible={menuVisible} />
)}
</MenuStateContext.Provider>
</ErrorBoundary>
);
}
return (
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton
key={relativePosition ? 'link' : 'main'}
ref={combinedRef}
tabIndex={0}
className={s(styles, { menuButton: true }, className)}
{...menu}
visible={menuVisible}
{...props}
>
<div className={s(styles, { box: true }, className)}>{children}</div>
</MenuButton>
<MenuPanel
ref={menuPanelRef}
label={label}
menu={menu}
rtl={rtl}
submenu={submenu}
panelAvailable={panelAvailable}
hasBindings={hasBindings}
getHasBindings={getHasBindings}
>
{items}
</MenuPanel>
{relativePosition && (
<MenuButtonLink ref={menuButtonLinkRef} className={s(styles, { menuButtonLink: true })} {...menu} visible={menuVisible} />
)}
</MenuStateContext.Provider>
</ErrorBoundary>
);
}),
);
@@ -1,35 +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.
*/
.iconButton {
overflow: hidden;
padding: 0;
margin: 0;
flex-shrink: 0;
}
.iconBox {
composes: theme-form-element-radius theme-text-primary theme-ripple from global;
box-sizing: border-box;
overflow: hidden;
padding: 4px !important;
margin: 2px !important;
flex-shrink: 0;
height: 24px !important;
display: flex;
cursor: pointer;
align-items: center;
gap: 2px;
user-select: none;
outline: none;
& .iconLabel {
text-transform: uppercase;
font-weight: 500;
}
}
@@ -1,38 +0,0 @@
/*
* 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 { ButtonProps } from 'reakit';
import { IconButton } from '../IconButton.js';
import { s } from '../s.js';
import { useS } from '../useS.js';
import moduleStyles from './MenuBarSmallItem.module.css';
interface Props extends Omit<ButtonProps, 'style' | 'icon'> {
icon?: string;
viewBox?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
export const MenuBarSmallItem: React.FC<React.PropsWithChildren<Props>> = function MenuBarSmallItem({
icon,
viewBox,
children,
className,
onClick,
...rest
}) {
const style = useS(moduleStyles);
// TODO: replace IconButton with StaticImage / Icon
// TODO: use button for icon-box (maybe)
return (
<div className={s(style, { iconBox: true }, className)} tabIndex={0} onClick={onClick}>
{icon && <IconButton className={s(style, { iconButton: true })} name={icon} viewBox={viewBox} {...rest} />}
{children && <div className={s(style, { iconLabel: true })}>{children}</div>}
</div>
);
};
@@ -1,24 +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 { observer } from 'mobx-react-lite';
import { useTranslate } from '../localization/useTranslate.js';
import { MenuItem } from './MenuItem.js';
import { MenuItemElement } from './MenuItemElement.js';
export const MenuEmptyItem = observer(function MenuEmptyItem() {
const translate = useTranslate();
const label = translate('core_ui_empty');
return (
<MenuItem id="empty" aria-label={label} disabled>
<MenuItemElement label={label} />
</MenuItem>
);
});
@@ -1,30 +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.
*/
.menuItem {
composes: theme-ripple from global;
composes: menuItem from './MenuItemElement.module.css';
display: flex;
align-items: center;
border: none;
padding: 0;
background: none;
text-align: left;
outline: none;
color: inherit;
cursor: pointer;
white-space: nowrap;
&.hidden {
display: none;
}
&:global([aria-selected='true']) {
opacity: 0.7;
}
}
@@ -1,55 +0,0 @@
/*
* 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 React, { useContext } from 'react';
import { MenuItem as ReakitMenuItem, type CompositeItemOptions } from 'reakit';
import { s } from '../s.js';
import { useCombinedHandler } from '../useCombinedHandler.js';
import { useS } from '../useS.js';
import style from './MenuItem.module.css';
import { MenuStateContext } from './MenuStateContext.js';
import type { ReakitProxyComponent, ReakitProxyComponentOptions } from './ReakitProxyComponent.js';
export type MenuItemOptions = CompositeItemOptions & {
selected?: boolean;
close?: boolean;
};
/**
* @deprecated use the @dbeaver/ui-kit package instead
*/
export const MenuItem: ReakitProxyComponent<'button', MenuItemOptions> = observer<ReakitProxyComponentOptions<'button', MenuItemOptions>>(
function MenuItem({ children, hidden, selected, close, onClick, className, ...rest }) {
const menu = useContext(MenuStateContext);
const styles = useS(style);
const handleClick = useCombinedHandler<[React.MouseEvent<HTMLButtonElement>]>(onClick, function handleClick() {
if (close) {
menu?.hide();
}
});
const MenuItem = ReakitMenuItem;
return (
<MenuItem
{...menu}
aria-selected={selected}
{...rest}
className={s(styles, { menuItem: true, hidden }, className)}
disabled={selected || rest.disabled}
style={{ pointerEvents: 'auto' }}
focusable
onClick={handleClick}
>
{children}
</MenuItem>
);
},
) as ReakitProxyComponent<'button', MenuItemOptions>;
@@ -1,34 +0,0 @@
/*
* 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.
*/
.menu {
outline: none;
z-index: 999;
}
.menuBox {
composes: theme-background-surface theme-text-on-surface theme-group-element-radius theme-typography--body2 theme-elevation-z5 from global;
composes: menuBox from './MenuItemElement.module.css';
display: flex;
flex-direction: column;
min-width: 140px;
padding: 6px 0;
& .menuBox {
margin-top: -6px;
}
}
.menu.modal .menuBox {
overflow: auto;
max-height: 80vh;
overflow-x: hidden;
}
.menu.submenu {
margin-top: -6px;
}
@@ -1,71 +0,0 @@
/*
* 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 { Children, forwardRef } from 'react';
import { Menu, type MenuStateReturn } from 'reakit';
import { ErrorBoundary } from '../ErrorBoundary.js';
import { getComputed } from '../getComputed.js';
import { useTranslate } from '../localization/useTranslate.js';
import { s } from '../s.js';
import { useS } from '../useS.js';
import { MenuEmptyItem } from './MenuEmptyItem.js';
import style from './MenuPanel.module.css';
import styleMenuItemElement from './MenuItemElement.module.css';
export interface IMenuPanelProps {
label: string;
menu: MenuStateReturn; // from reakit useMenuState
panelAvailable?: boolean;
hasBindings?: boolean;
getHasBindings?: () => boolean;
children: React.ReactNode | (() => React.ReactNode);
rtl?: boolean;
submenu?: boolean;
className?: string;
}
/**
* @deprecated use the @dbeaver/ui-kit package instead
*/
export const MenuPanel = observer<IMenuPanelProps, HTMLDivElement>(
forwardRef(function MenuPanel({ label, menu, submenu, panelAvailable = true, rtl, getHasBindings, hasBindings, children, className }, ref) {
const translate = useTranslate();
const styles = useS(style, styleMenuItemElement);
const visible = menu.visible;
if (!visible) {
return null;
}
hasBindings = panelAvailable && (hasBindings || getComputed(() => getHasBindings?.()));
let renderedChildren: React.ReactNode = <></>;
if (panelAvailable) {
renderedChildren = typeof children === 'function' ? children() : children;
}
return (
<ErrorBoundary>
<Menu
ref={ref}
className={s(styles, { menu: true, modal: menu.modal, submenu }, className)}
{...menu}
aria-label={translate(label)}
visible={panelAvailable}
>
<div dir={rtl ? 'rtl' : undefined} data-s-has-bindings={hasBindings} className={s(styles, { menuBox: true })}>
{Children.count(renderedChildren) === 0 && <MenuEmptyItem />}
{renderedChildren}
</div>
</Menu>
</ErrorBoundary>
);
}),
);
@@ -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.
*/
import { createContext } from 'react';
import type { MenuStateReturn } from 'reakit';
export type IMenuState = MenuStateReturn;
export const MenuStateContext = createContext<IMenuState | null>(null);
@@ -1,22 +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 type { As, PropsWithAs } from 'reakit-utils/types';
export type ReakitProxyPropsWithAs<O, T extends As> = PropsWithAs<O, T>;
export type ReakitProxyComponentOptions<T extends As, O> = ReakitProxyPropsWithAs<O, T>;
export type ReakitProxyComponent<T extends As, O> = {
<TT extends As = T>(
props: ReakitProxyPropsWithAs<O, TT> & {
as: TT;
},
context?: any,
): React.ReactElement<any, any> | null;
(props: ReakitProxyPropsWithAs<O, T>, context?: any): React.ReactElement<any, any> | null;
};
@@ -37,10 +37,11 @@ interface Props {
staticProperties?: boolean;
filterable?: boolean;
sortByName?: boolean;
disableOverflowEffect?: boolean;
}
export const PropertiesTable = observer<Props>(function PropertiesTable(props) {
const { className, onAdd, readOnly, propertiesState } = props;
const { className, onAdd, readOnly, propertiesState, disableOverflowEffect = false } = props;
const translate = useTranslate();
const propsRef = useObjectRef({ ...props });
const style = useS(styles);
@@ -165,7 +166,7 @@ export const PropertiesTable = observer<Props>(function PropertiesTable(props) {
onRemove={removeProperty}
/>
))}
<div className={s(style, { propertiesListOverflow: true })} />
{!disableOverflowEffect && <div className={s(style, { propertiesListOverflow: true })} />}
</div>
</div>
);
@@ -1,11 +1,9 @@
/*
* 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.
*/
.menu {
display: none !important;
}
export const SLIDE_PANEL_CLOSE_BUTTON_ID = 'slide-panel-close-button';
@@ -1,12 +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.
*/
.slideBox {
overflow: hidden;
white-space: nowrap;
position: relative;
}
@@ -1,61 +0,0 @@
/*
* 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 { useEffect, useRef } from 'react';
import { s, useHotkeys, useMergeRefs, useS } from '../index.js';
import SlideBoxStyles from './SlideBox.module.css';
import SlideBoxElementStyles from './SlideElement.module.css';
import SlideBoxOverlayStyles from './SlideOverlay.module.css';
interface Props {
className?: string;
children?: React.ReactNode;
open?: boolean;
onClose?: () => void;
}
export const SlideBox = observer<Props>(function SlideBox({ children, open, className, onClose }) {
const slideBoxStyles = useS(SlideBoxStyles);
const slideBoxElementStyles = useS(SlideBoxElementStyles);
const slideBoxOverlayStyles = useS(SlideBoxOverlayStyles);
const divRef = useRef<HTMLDivElement>(null);
const ref = useHotkeys('escape', () => onClose?.(), { enabled: open });
const mergedRefs = useMergeRefs(ref, divRef);
useEffect(() => {
const div = divRef.current;
function handleScroll() {
if (div) {
div.scrollLeft = 0;
div.scrollTop = 0;
}
}
if (div) {
div.addEventListener('scroll', handleScroll);
}
return () => {
if (div) {
div.removeEventListener('scroll', handleScroll);
}
};
}, []);
return (
<div
ref={mergedRefs}
className={s(slideBoxStyles, { slideBox: true }, s(slideBoxElementStyles, { open }), s(slideBoxOverlayStyles, { open }), className)}
>
{children}
</div>
);
});
@@ -1,28 +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.
*/
.slideElement {
width: 100%;
height: 100%;
display: inline-block;
vertical-align: top;
white-space: normal;
position: relative;
&:last-child {
width: calc(100% - 120px);
transition: transform ease-in-out 0.3s;
will-change: transform;
z-index: 1;
}
}
.open .slideElement {
&:last-child {
transform: translateX(-100%);
}
}
@@ -1,29 +0,0 @@
/*
* 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 { s } from '../s.js';
import { useS } from '../useS.js';
import style from './SlideElement.module.css';
interface Props {
className?: string;
children?: React.ReactNode;
open?: boolean;
inert?: boolean;
}
export const SlideElement = observer<Props>(function SlideElement({ children, className, inert }) {
const styles = useS(style);
return (
<div className={s(styles, { slideElement: true }, className)} inert={inert}>
{children}
</div>
);
});
@@ -1,48 +0,0 @@
@keyframes enableEvents {
to {
pointer-events: auto;
}
}
.slideOverlay {
z-index: 1;
position: absolute;
cursor: pointer;
display: flex;
align-items: center;
width: 0;
height: 0;
top: 0;
left: 0;
background: rgb(0 0 0 / 0%);
transition: background ease-in-out 0.3s;
pointer-events: none;
}
.iconBtn {
composes: theme-background-surface theme-text-on-surface theme-ripple from global;
box-sizing: border-box;
margin-left: 36px;
width: 48px;
height: 48px;
padding: 16px;
border-radius: 50%;
display: none;
overflow: hidden;
align-items: center;
transform: rotate(90deg);
}
.icon {
width: 100%;
}
.open .slideOverlay {
width: 100%;
height: 100%;
background: rgb(0 0 0 / 35%);
animation: enableEvents 0.3s forwards;
& > div {
display: flex;
}
}
@@ -1,24 +0,0 @@
/*
* 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 { s } from '../s.js';
import { useS } from '../useS.js';
import style from './SlideOverlay.module.css';
interface Props {
className?: string;
onClick?: () => void;
open?: boolean;
}
export const SlideOverlay = observer<Props>(function SlideOverlay({ className, onClick }) {
const styles = useS(style);
return <div className={s(styles, { slideOverlay: true }, className)} onClick={onClick} />;
});
@@ -1,25 +0,0 @@
.iconBtn {
position: absolute;
left: 0;
top: 50%;
z-index: 1;
transform: translateY(-50%) translateX(-175%) rotate(90deg);
background-color: var(--theme-surface);
box-sizing: border-box;
width: 48px;
height: 48px;
padding: 15px;
border-radius: 50%;
color: var(--theme-on-surface);
overflow: hidden;
align-items: center;
&:hover,
&:focus {
background-color: color-mix(in srgb, var(--theme-on-surface), var(--theme-surface) 90%);
}
}
.loader {
height: 100%;
}
@@ -1,45 +0,0 @@
/*
* 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 { IconButton } from '@dbeaver/ui-kit';
import { SlideElement, Icon, Loader, s, useS, useTranslate } from '@cloudbeaver/core-blocks';
interface SlidePanelProps {
children?: React.ReactNode;
isOpen: boolean;
onClose: () => void;
}
import style from './SlidePanel.module.css';
export const SLIDE_PANEL_CLOSE_BUTTON_ID = 'slide-panel-close-button';
export function SlidePanel({ children, isOpen, onClose }: SlidePanelProps): React.ReactElement {
const styles = useS(style);
const t = useTranslate();
return (
<SlideElement open={isOpen}>
{isOpen && (
<IconButton
id={SLIDE_PANEL_CLOSE_BUTTON_ID}
size="small"
aria-label={t('core_blocks_dialog_element_close_tooltip')}
className={s(styles, { iconBtn: true })}
autoFocus
onClick={onClose}
>
<Icon name="cross" viewBox="0 0 24 24" />
</IconButton>
)}
<Loader className={s(styles, { loader: true })} suspense>
{children}
</Loader>
</SlideElement>
);
}
@@ -0,0 +1,47 @@
@layer components {
.dbv-slide-dialog__overlay {
position: absolute;
inset: 0;
z-index: 999;
background: rgb(0 0 0 / 0%);
transition: background ease-in-out 0.3s;
pointer-events: none;
&[data-active] {
background: rgb(0 0 0 / 35%);
pointer-events: auto;
cursor: pointer;
}
}
.dbv-slide-dialog__dialog {
--dbv-kit-dialog-content-background: var(--theme-surface);
overflow: visible;
}
.dbv-slide-dialog__close-btn {
position: absolute;
left: 0;
top: 50%;
z-index: 1;
transform: translateY(-50%) translateX(-175%) rotate(90deg);
background-color: var(--theme-surface);
box-sizing: border-box;
width: 48px;
height: 48px;
padding: 15px;
border-radius: 50%;
color: var(--theme-on-surface);
overflow: hidden;
align-items: center;
&:hover,
&:focus {
background-color: color-mix(in srgb, var(--theme-on-surface), var(--theme-surface) 90%);
}
}
.dbv-slide-dialog__loader {
height: 100%;
}
}
@@ -0,0 +1,67 @@
/*
* 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 { useCallback, useRef } from 'react';
import { Dialog, IconButton } from '@dbeaver/ui-kit';
import { Icon } from '../Icon.js';
import { Loader } from '../Loader/Loader.js';
import { SLIDE_PANEL_CLOSE_BUTTON_ID } from '../SLIDE_PANEL_CLOSE_BUTTON_ID.js';
import { useTranslate } from '../localization/useTranslate.js';
import './SlideDialog.css';
export interface SlideDialogProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const SlideDialog = observer<SlideDialogProps>(function SlideDialog({ open, onClose, children }) {
const t = useTranslate();
const overlayRef = useRef<HTMLDivElement>(null);
// We need to specify persistent elements for the dialog to make them accessible when the dialog is open.
// To add an element to the list of persistent elements, add the `data-dialog-persistent-element` attribute to it.
const getPersistentElements = useCallback(() => {
const dialogs = document.querySelectorAll('[data-dialog]');
const persistentElements = document.querySelectorAll('[data-dialog-persistent-element]');
return [...dialogs, ...persistentElements];
}, []);
return (
<>
<div ref={overlayRef} className="dbv-slide-dialog__overlay" data-active={open || undefined} data-dialog-persistent-element onClick={onClose} />
<Dialog
open={open}
data-variant="slide"
portal={false}
backdrop={false}
preventBodyScroll={false}
hideOnInteractOutside={false}
getPersistentElements={getPersistentElements}
className="dbv-slide-dialog__dialog"
onClose={onClose}
>
<IconButton
id={SLIDE_PANEL_CLOSE_BUTTON_ID}
size="small"
aria-label={t('core_blocks_dialog_element_close_tooltip')}
className="dbv-slide-dialog__close-btn"
onClick={onClose}
>
<Icon name="cross" viewBox="0 0 24 24" />
</IconButton>
<Loader className="dbv-slide-dialog__loader" suspense>
{children}
</Loader>
</Dialog>
</>
);
});
+3 -13
View File
@@ -58,17 +58,8 @@ export * from './ConnectionImageWithMask/ConnectionImageWithMask.js';
export { default as ConnectionImageWithMaskSvgStyles } from './ConnectionImageWithMask/ConnectionImageWithMaskSvg.module.css';
export * from './Menu/AriaKit/MenuAriaKit.js';
export * from './Menu/Menu.js';
export { default as MenuStyles } from './Menu/Menu.module.css';
export * from './Menu/MenuBarSmallItem.js';
export * from './Menu/MenuEmptyItem.js';
export * from './Menu/MenuItem.js';
export { default as MenuItemStyles } from './Menu/MenuItem.module.css';
export * from './Menu/MenuItemElement.js';
export { default as MenuItemElementStyles } from './Menu/MenuItemElement.module.css';
export * from './Menu/MenuPanel.js';
export { default as MenuPanelStyles } from './Menu/MenuPanel.module.css';
export * from './Menu/MenuStateContext.js';
export * from './Menu/useContextMenuPosition.js';
export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoFormLoader.js';
@@ -89,10 +80,8 @@ export * from './Placeholder/PlaceholderContainer.js';
export * from './PropertiesTable/PropertiesTable.js';
export * from './PropertiesTable/IProperty.js';
export * from './Slide/SlideBox.js';
export * from './Slide/SlideElement.js';
export * from './Slide/SlideOverlay.js';
export * from './Slide/SlidePanel.js';
export * from './SLIDE_PANEL_CLOSE_BUTTON_ID.js';
export * from './SlideDialog/SlideDialog.js';
export * from './Split/SplitControls.js';
export * from './Split/Pane.js';
@@ -190,6 +179,7 @@ export * from './FormControls/Form.js';
export * from './FormControls/Textarea.js';
export * from './FormControls/useCapsLockTracker.js';
export * from './FormControls/useCustomInputValidation.js';
export * from './FormControls/useFormCustomInputValidation.js';
export * from './FormControls/useForm.js';
export * from './FormControls/Textarea.js';
export * from './Link.js';
@@ -1,6 +1,6 @@
/*
* 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.
@@ -16,5 +16,9 @@ interface Props extends React.PropsWithChildren {
export const TopAppBar: React.FC<Props> = function TopAppBar({ children, className }) {
const style = useS(styles);
return <header className={s(style, { header: true }, className)}>{children}</header>;
return (
<header className={s(style, { header: true }, className)} data-dialog-persistent-element>
{children}
</header>
);
};
@@ -1,6 +1,6 @@
/*
* 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.
@@ -9,21 +9,22 @@ import { PasswordPolicyService } from '@cloudbeaver/core-authentication';
import { useService } from '@cloudbeaver/core-di';
import { PasswordPolicyResource } from '@cloudbeaver/core-root';
import { useCustomInputValidation } from './FormControls/useCustomInputValidation.js';
import { useFormCustomInputValidation } from './FormControls/useFormCustomInputValidation.js';
import { useResource } from './ResourcesHooks/useResource.js';
import type { IFormContext } from './FormControls/FormContext.js';
export function usePasswordValidation() {
export function usePasswordValidation(formContext?: IFormContext) {
useResource(usePasswordValidation, PasswordPolicyResource, undefined);
const passwordPolicyService = useService(PasswordPolicyService);
const ref = useCustomInputValidation<string>(value => {
const { ref } = useFormCustomInputValidation<string>(value => {
if (!value) {
return null;
}
const validation = passwordPolicyService.validatePassword(value);
return validation.isValid ? null : validation.errorMessage;
});
}, formContext);
return ref;
}
@@ -25,11 +25,11 @@
"@cloudbeaver/core-blocks": "workspace:*",
"@cloudbeaver/core-di": "workspace:*",
"@cloudbeaver/core-events": "workspace:*",
"@dbeaver/ui-kit": "workspace:^",
"mobx": "^6",
"mobx-react-lite": "^4",
"react": "^19",
"react-dom": "^19",
"reakit": "^1",
"tslib": "^2"
},
"devDependencies": {
@@ -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 { observer } from 'mobx-react-lite';
import { Portal } from 'reakit';
import { s, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { Portal } from '@dbeaver/ui-kit';
import styles from './Notifications.module.css';
import { NotificationsItem } from './NotificationsItem/NotificationsItem.js';
@@ -21,7 +22,7 @@ export const Notifications = observer(function Notifications() {
return (
<Portal>
<div className={s(style, { notifications: true })}>
<div className={s(style, { notifications: true })} data-dialog-persistent-element>
{notificationService.visibleNotifications.map(notification => (
<NotificationsItem key={notification.id} notification={notification} />
))}
@@ -7,6 +7,9 @@
"composite": true
},
"references": [
{
"path": "../../common-react/@dbeaver/ui-kit"
},
{
"path": "../core-blocks"
},
@@ -24,7 +24,7 @@ export function getObjectPropertyDisplayValue(property: IObjectPropertyInfo) {
return String(getValue(property.value, controlType));
}
function getValue(value: any, controlType: ObjectPropertyType) {
function getValue(value: any, controlType: ObjectPropertyType): any {
const checkbox = controlType === 'checkbox';
if (value === null || value === undefined) {
@@ -40,7 +40,7 @@ function getValue(value: any, controlType: ObjectPropertyType) {
}
if (Array.isArray(value)) {
return value.join(', ');
return `[${value.map(v => getValue(v, controlType)).join(', ')}]`;
}
return value.value || value.displayName || JSON.stringify(value);
-1
View File
@@ -42,7 +42,6 @@
"react-dnd": "^16",
"react-dnd-html5-backend": "^16",
"react-dom": "^19",
"reakit": "^1",
"tslib": "^2"
},
"devDependencies": {
@@ -14,7 +14,7 @@ import { MenuButton, MenuProvider, Menu, type HovercardStoreState, useMenuStore,
import { RenderMenuItems } from './RenderMenuItems.js';
import { type IMenuContext, MenuContext } from './MenuContext.js';
export interface IContextMenuNewProps extends Omit<IContextMenuProps, 'placement'> {
export interface IContextMenuNewProps extends IContextMenuProps {
render?: React.ReactElement;
placement?: HovercardStoreState['placement'];
showOnHover?: boolean;
@@ -1,12 +1,12 @@
/*
* 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 type { ButtonHTMLAttributes } from 'react';
import type { MenuInitialState } from 'reakit';
import type { IContextMenuPosition } from '@cloudbeaver/core-blocks';
import type { IMenuData } from '@cloudbeaver/core-view';
@@ -21,7 +21,6 @@ export type ContextMenuRenderingChildren = (props: IContextMenuBaseProps) => Rea
export interface IContextMenuProps extends Omit<ButtonHTMLAttributes<any>, 'children'> {
contextMenuPosition?: IContextMenuPosition;
menu: IMenuData;
placement?: MenuInitialState['placement'];
visible?: boolean;
rtl?: boolean;
children?: React.ReactNode;
@@ -1,6 +1,6 @@
/*
* 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.
@@ -51,7 +51,7 @@ export const SideBarPanel = observer<SideBarPanelProps>(function SideBarPanel({
onChange={tab => selectTab(tab.tabId)}
onReorder={onReorder}
>
<div className={s(style, { box: true })}>
<div className={s(style, { box: true })} data-panel-id={panelId} data-dialog-persistent-element>
<TabList className={s(style, { tabList: true })} underline />
<div className={s(style, { contentBox: true })}>
<TabPanelList />
@@ -125,7 +125,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
const tabExists = isNotNullDefined(selectedId) && displayed.includes(selectedId);
if (!tabExists) {
dynamic.store.select(displayed[0]);
dynamic.store.setSelectedId(displayed[0]);
}
}
}, [displayed, autoSelect]);
@@ -141,7 +141,7 @@ export const TabsState = observer(function TabsState<T = Record<string, any>>({
}
dynamic.selectedId = data.tabId;
if (dynamic.store.getState().selectedId !== data.tabId) {
dynamic.store.select(data.tabId);
dynamic.store.setSelectedId(data.tabId);
}
},
],
+1
View File
@@ -23,6 +23,7 @@
"validate-dependencies": "core-cli-validate-dependencies"
},
"dependencies": {
"@dbeaver/js-helpers": "workspace:*",
"@timohausmann/quadtree-ts": "^2",
"fast-deep-equal": "^3",
"md5": "^2",
+2 -17
View File
@@ -1,24 +1,9 @@
/*
* 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 function formatNumber(n: number, d: number) {
if (n < 1000) {
return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
const numStr = n.toString();
const exponent = numStr.length - (numStr.length % 3);
const power = Math.pow(10, d);
const rounded = Math.round((n * power) / Math.pow(10, exponent)) / power;
const units = ' kMBTPE';
const unit = units[exponent / 3]!;
return rounded + unit;
}
export { formatNumber } from '@dbeaver/js-helpers';
+3
View File
@@ -13,6 +13,9 @@
{
"path": "../../common-typescript/@dbeaver/cli"
},
{
"path": "../../common-typescript/@dbeaver/js-helpers"
},
{
"path": "../core-cli"
},
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 6.5C10.5 8.70914 8.70914 10.5 6.5 10.5C4.29086 10.5 2.5 8.70914 2.5 6.5C2.5 4.29086 4.29086 2.5 6.5 2.5C8.70914 2.5 10.5 4.29086 10.5 6.5ZM9.68256 10.9862C8.78427 11.6246 7.68597 12 6.5 12C3.46243 12 1 9.53757 1 6.5C1 3.46243 3.46243 1 6.5 1C9.53757 1 12 3.46243 12 6.5C12 7.88792 11.4859 9.15577 10.6378 10.1235L10.7678 10.1464L15.0104 14.3891L13.9497 15.4497L9.70711 11.2071L9.68256 10.9862ZM4.8 7.29999V5.79999H5.8V4.79999H7.3V5.79999H8.3V7.29999H7.3V8.29999H5.8V7.29999H4.8Z" fill="#398FCA"/>
</svg>

Before

Width:  |  Height:  |  Size: 655 B

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 10.5C8.70914 10.5 10.5 8.70914 10.5 6.5C10.5 4.29086 8.70914 2.5 6.5 2.5C4.29086 2.5 2.5 4.29086 2.5 6.5C2.5 8.70914 4.29086 10.5 6.5 10.5ZM6.5 12C7.68597 12 8.78427 11.6246 9.68256 10.9862L9.70711 11.2071L13.9497 15.4497L15.0104 14.3891L10.7678 10.1464L10.6378 10.1235C11.4859 9.15577 12 7.88792 12 6.5C12 3.46243 9.53757 1 6.5 1C3.46243 1 1 3.46243 1 6.5C1 9.53757 3.46243 12 6.5 12ZM4.8 5.79999V7.29999H8.3V5.79999H4.8Z" fill="#398FCA"/>
</svg>

Before

Width:  |  Height:  |  Size: 598 B

@@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7928_11650)">
<path d="M12.0001 3.00001C16.9601 3.00001 21.0001 7.04003 21.0001 12.0001C21.0001 16.9601 16.9601 21.0001 12.0001 21.0001C7.04003 21.0001 3.00001 16.9601 3.00001 12.0001C3.00001 7.04003 7.04003 3.00001 12.0001 3.00001ZM12.0001 1C5.92002 1 1 5.92002 1 12.0001C1 18.0801 5.92002 23.0001 12.0001 23.0001C18.0801 23.0001 23.0001 18.0801 23.0001 12.0001C23.0001 5.92002 18.0801 1 12.0001 1Z" fill="#3387C5"/>
<path d="M19 19L30.0001 30" stroke="#3387C5" stroke-width="2" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_7928_11650">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 742 B

@@ -0,0 +1,13 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7923_11529)">
<path d="M12.0001 3.00001C16.9601 3.00001 21.0001 7.04003 21.0001 12.0001C21.0001 16.9601 16.9601 21.0001 12.0001 21.0001C7.04003 21.0001 3.00001 16.9601 3.00001 12.0001C3.00001 7.04003 7.04003 3.00001 12.0001 3.00001ZM12.0001 1C5.92002 1 1 5.92002 1 12.0001C1 18.0801 5.92002 23.0001 12.0001 23.0001C18.0801 23.0001 23.0001 18.0801 23.0001 12.0001C23.0001 5.92002 18.0801 1 12.0001 1Z" fill="#3387C5"/>
<path d="M19 19L30.0001 30" stroke="#3387C5" stroke-width="2" stroke-miterlimit="10"/>
<path d="M13 6H11V18.0001H13V6Z" fill="#3387C5"/>
<path d="M18.0001 11H6V13H18.0001V11Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_7923_11529">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 848 B

@@ -0,0 +1,13 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7923_11590)">
<path d="M9.64291 2.57142C13.5401 2.57142 16.7144 5.74569 16.7144 9.64282C16.7144 13.5399 13.5401 16.7142 9.64291 16.7142C5.74574 16.7142 2.57144 13.5399 2.57144 9.64282C2.57144 5.74569 5.74574 2.57142 9.64291 2.57142ZM9.64291 1C4.86574 1 1 4.8657 1 9.64282C1 14.4199 4.86574 18.2856 9.64291 18.2856C14.4201 18.2856 18.2858 14.4199 18.2858 9.64282C18.2858 4.8657 14.4201 1 9.64291 1Z" fill="#3387C5"/>
<path d="M15.1426 15.1426L22.9998 22.9997" stroke="#3387C5" stroke-width="1.5" stroke-miterlimit="10"/>
<path d="M10.4279 4.92871H8.85645V14.3572H10.4279V4.92871Z" fill="#3387C5"/>
<path d="M14.3573 8.85645H4.92871V10.4279H14.3573V8.85645Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_7923_11590">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 911 B

@@ -0,0 +1,18 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7923_11544)">
<g clip-path="url(#clip1_7923_11544)">
<path d="M6.28559 1.14283C9.11982 1.14283 11.4283 3.45136 11.4283 6.28559C11.4283 9.11982 9.11982 11.4283 6.28559 11.4283C3.45136 11.4283 1.14283 9.11982 1.14283 6.28559C1.14283 3.45136 3.45136 1.14283 6.28559 1.14283ZM6.28559 0C2.81137 0 0 2.81137 0 6.28559C0 9.7598 2.81137 12.5712 6.28559 12.5712C9.7598 12.5712 12.5712 9.7598 12.5712 6.28559C12.5712 2.81137 9.7598 0 6.28559 0Z" fill="#3387C5"/>
<path d="M10.2861 10.2861L16.0003 16.0003" stroke="#3387C5" stroke-miterlimit="10"/>
<path d="M6.85768 2.85645H5.71484V9.71345H6.85768V2.85645Z" fill="#3387C5"/>
<path d="M9.71345 5.71484H2.85645V6.85768H9.71345V5.71484Z" fill="#3387C5"/>
</g>
</g>
<defs>
<clipPath id="clip0_7923_11544">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1_7923_11544">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1023 B

@@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7928_11656)">
<path d="M9.64291 2.57142C13.5401 2.57142 16.7144 5.74569 16.7144 9.64282C16.7144 13.5399 13.5401 16.7142 9.64291 16.7142C5.74574 16.7142 2.57144 13.5399 2.57144 9.64282C2.57144 5.74569 5.74574 2.57142 9.64291 2.57142ZM9.64291 1C4.86574 1 1 4.8657 1 9.64282C1 14.4199 4.86574 18.2856 9.64291 18.2856C14.4201 18.2856 18.2858 14.4199 18.2858 9.64282C18.2858 4.8657 14.4201 1 9.64291 1Z" fill="#3387C5"/>
<path d="M15.1426 15.1426L22.9998 22.9997" stroke="#3387C5" stroke-width="1.5" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_7928_11656">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 757 B

@@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7928_11629)">
<path d="M12.0001 3.00001C16.9601 3.00001 21.0001 7.04003 21.0001 12.0001C21.0001 16.9601 16.9601 21.0001 12.0001 21.0001C7.04003 21.0001 3.00001 16.9601 3.00001 12.0001C3.00001 7.04003 7.04003 3.00001 12.0001 3.00001ZM12.0001 1C5.92002 1 1 5.92002 1 12.0001C1 18.0801 5.92002 23.0001 12.0001 23.0001C18.0801 23.0001 23.0001 18.0801 23.0001 12.0001C23.0001 5.92002 18.0801 1 12.0001 1Z" fill="#3387C5"/>
<path d="M19 19L30.0001 30" stroke="#3387C5" stroke-width="2" stroke-miterlimit="10"/>
<path d="M18.0001 11H6V13H18.0001V11Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_7928_11629">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 798 B

@@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7928_11636)">
<path d="M9.64291 2.57142C13.5401 2.57142 16.7144 5.74569 16.7144 9.64282C16.7144 13.5399 13.5401 16.7142 9.64291 16.7142C5.74574 16.7142 2.57144 13.5399 2.57144 9.64282C2.57144 5.74569 5.74574 2.57142 9.64291 2.57142ZM9.64291 1C4.86574 1 1 4.8657 1 9.64282C1 14.4199 4.86574 18.2856 9.64291 18.2856C14.4201 18.2856 18.2858 14.4199 18.2858 9.64282C18.2858 4.8657 14.4201 1 9.64291 1Z" fill="#3387C5"/>
<path d="M15.1426 15.1426L22.9998 22.9997" stroke="#3387C5" stroke-width="1.5" stroke-miterlimit="10"/>
<path d="M14.3573 8.85645H4.92871V10.4279H14.3573V8.85645Z" fill="#3387C5"/>
</g>
<defs>
<clipPath id="clip0_7928_11636">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 834 B

@@ -0,0 +1,17 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7928_11643)">
<g clip-path="url(#clip1_7928_11643)">
<path d="M6.28559 1.14283C9.11982 1.14283 11.4283 3.45136 11.4283 6.28559C11.4283 9.11982 9.11982 11.4283 6.28559 11.4283C3.45136 11.4283 1.14283 9.11982 1.14283 6.28559C1.14283 3.45136 3.45136 1.14283 6.28559 1.14283ZM6.28559 0C2.81137 0 0 2.81137 0 6.28559C0 9.7598 2.81137 12.5712 6.28559 12.5712C9.7598 12.5712 12.5712 9.7598 12.5712 6.28559C12.5712 2.81137 9.7598 0 6.28559 0Z" fill="#3387C5"/>
<path d="M10.2861 10.2861L16.0003 16.0003" stroke="#3387C5" stroke-miterlimit="10"/>
<path d="M9.71345 5.71484H2.85645V6.85768H9.71345V5.71484Z" fill="#3387C5"/>
</g>
</g>
<defs>
<clipPath id="clip0_7928_11643">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1_7928_11643">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 946 B

@@ -0,0 +1,16 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7928_11662)">
<g clip-path="url(#clip1_7928_11662)">
<path d="M6.28559 1.14283C9.11982 1.14283 11.4283 3.45136 11.4283 6.28559C11.4283 9.11982 9.11982 11.4283 6.28559 11.4283C3.45136 11.4283 1.14283 9.11982 1.14283 6.28559C1.14283 3.45136 3.45136 1.14283 6.28559 1.14283ZM6.28559 0C2.81137 0 0 2.81137 0 6.28559C0 9.7598 2.81137 12.5712 6.28559 12.5712C9.7598 12.5712 12.5712 9.7598 12.5712 6.28559C12.5712 2.81137 9.7598 0 6.28559 0Z" fill="#3387C5"/>
<path d="M10.2861 10.2861L16.0003 16.0003" stroke="#3387C5" stroke-miterlimit="10"/>
</g>
</g>
<defs>
<clipPath id="clip0_7928_11662">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1_7928_11662">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 869 B

@@ -1,6 +1,6 @@
/*
* 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.
@@ -10,5 +10,5 @@ import { createAction } from '../createAction.js';
export const ACTION_ZOOM_IN = createAction('zoom-in', {
label: 'core_view_zoom_in',
tooltip: 'core_view_zoom_in',
icon: '/icons/zoom-in.svg',
icon: '/icons/zoom_in.svg',
});
@@ -1,6 +1,6 @@
/*
* 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.
@@ -10,5 +10,5 @@ import { createAction } from '../createAction.js';
export const ACTION_ZOOM_OUT = createAction('zoom-out', {
label: 'core_view_zoom_out',
tooltip: 'core_view_zoom_out',
icon: '/icons/zoom-out.svg',
icon: '/icons/zoom_out.svg',
});
@@ -1,6 +1,6 @@
/*
* 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.
@@ -14,19 +14,7 @@ import {
filterOnlyActive,
type IAdministrationItemRoute,
} from '@cloudbeaver/core-administration';
import {
s,
SContext,
SlideBox,
SlideElement,
SlideOverlay,
type StyleRegistry,
ToolsActionStyles,
ToolsPanelStyles,
useAutoLoad,
useS,
SlidePanel,
} from '@cloudbeaver/core-blocks';
import { s, SContext, SlideDialog, type StyleRegistry, ToolsActionStyles, ToolsPanelStyles, useAutoLoad, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { OptionsPanelService, TabList, TabListStyles, TabsState, TabStyles } from '@cloudbeaver/core-ui';
import type { ILoadableState } from '@cloudbeaver/core-utils';
@@ -118,7 +106,7 @@ export const Administration = observer<React.PropsWithChildren<Props>>(function
<AdministrationCaptureViewContext />
<TabsState currentTabId={activeScreen?.item} localState={administrationScreenService.itemState} orientation="vertical">
<SContext registry={tabsRegistry}>
<TabList aria-label="Administration items" vertical>
<TabList aria-label="Administration items" data-dialog-persistent-element vertical>
{visibleItems.map(item => (
<DrawerItem
key={item.name}
@@ -139,19 +127,16 @@ export const Administration = observer<React.PropsWithChildren<Props>>(function
<div ref={contentRef} className={s(styles, { contentContainer: true })}>
{children}
<SContext registry={adminPageRegistry}>
<SlideBox className={s(styles, { slideBox: true })} open={optionsPanelService.active} onClose={close}>
<SlideElement inert={optionsPanelService.active} className={s(styles, { slideElement: true })}>
<div className={s(styles, { content: true })}>
<ItemContent activeScreen={activeScreen} configurationWizard={configurationWizard} />
</div>
</SlideElement>
<SlideOverlay onClick={close} />
<SlidePanel isOpen={optionsPanelService.active} onClose={close}>
<div className={s(styles, { container: true })}>
<div className={s(styles, { mainContent: true })}>
<ItemContent activeScreen={activeScreen} configurationWizard={configurationWizard} />
</div>
<SlideDialog open={optionsPanelService.active} onClose={close}>
<div className={s(styles, { content: true })}>
<OptionsPanel />
</div>
</SlidePanel>
</SlideBox>
</SlideDialog>
</div>
</SContext>
</div>
</TabsState>

Some files were not shown because too many files have changed in this diff Show More