Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446bd81e08 | ||
|
|
09863ea260 | ||
|
|
34b4556773 | ||
|
|
9b4c874e51 | ||
|
|
ddbc9789e7 | ||
|
|
810d0215cb | ||
|
|
3dadc17ff1 | ||
|
|
8c69985083 | ||
|
|
1d5b475482 | ||
|
|
b779b9d855 | ||
|
|
df676913a0 | ||
|
|
fab9d1dd43 | ||
|
|
36dfdc8b33 | ||
|
|
8b0a20cfd7 | ||
|
|
9553c30ee5 | ||
|
|
9c36651aa0 | ||
|
|
24a251cd5b | ||
|
|
a44ba89cd9 | ||
|
|
44077ad27a | ||
|
|
3ae74763ff | ||
|
|
45a63f70ee | ||
|
|
1bafe29e99 | ||
|
|
10ad601f8b |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||