mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 14:50:15 +08:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f2a005598 | ||
|
|
40385f3b1c | ||
|
|
4bc74883e0 | ||
|
|
33d1d97fb7 | ||
|
|
296e03ec74 | ||
|
|
2abdbce8ff | ||
|
|
051695451b | ||
|
|
5b7a7fc3d6 | ||
|
|
0ba013e7ad | ||
|
|
2d2f7476b4 | ||
|
|
23a0ad49b9 | ||
|
|
d21a5aac7f | ||
|
|
f9cd51db0f | ||
|
|
3687a645b6 | ||
|
|
c142996db0 | ||
|
|
36d2b46496 | ||
|
|
cb8b1cfc52 | ||
|
|
743afcaad7 | ||
|
|
52c78e1aee | ||
|
|
ff275c36b6 | ||
|
|
9a94639167 | ||
|
|
74d09ee4dc | ||
|
|
360aaed795 | ||
|
|
912ca3fdcd | ||
|
|
32bbfc3a37 | ||
|
|
1e05ad9eea | ||
|
|
4b516c770d | ||
|
|
4e1fed8177 | ||
|
|
4b30d1073b | ||
|
|
269128360d | ||
|
|
07db19635e | ||
|
|
41d9ac4296 | ||
|
|
a536a93ea3 | ||
|
|
aefdca73fa | ||
|
|
5d402d0c20 | ||
|
|
fcd1924853 | ||
|
|
05c0aafb53 | ||
|
|
159b1b8bd6 | ||
|
|
f03c08bde7 | ||
|
|
1b07a66192 | ||
|
|
ce1686e0eb | ||
|
|
091160a59a | ||
|
|
47d2377b39 |
@@ -25,91 +25,55 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io
|
||||
|
||||
## Changelog
|
||||
|
||||
### 25.3.5 2026-02-16
|
||||
### 26.0.0 2026-03-02
|
||||
|
||||
- Data Editor:
|
||||
- Added the ability to automatically generate INSERT, SELECT, DELETE, and UPDATE statements for the selected values in the Data Editor;
|
||||
- Added a status indicator icon in the top-left corner of the Data Editor with tooltips explaining table editability. The icon shows whether the table has a primary key, is read-only due to connection settings, or contains only read-only columns.
|
||||
- Navigator:
|
||||
- The Connection Info tab was added so that all users can see basic information about the current connection;
|
||||
- Added the ability to duplicate connection configuration in the project navigation tree. The "Clone connection" feature is available in the context menu.
|
||||
- General:
|
||||
- The missing pg_dump utility no longer causes errors during initialization or during deployment updates when PostgreSQL is selected as the inner database;
|
||||
- DuckDB: Driver version was updated to 1.4.4.0;
|
||||
- PostgreSQL: Added DDL display support for PostgreSQL policies.
|
||||
### Changes since 25.3.0:
|
||||
|
||||
### 25.3.4 2026-02-02
|
||||
|
||||
- Editors:
|
||||
- Added undo and redo functionality to the Data Editor for cell edits, row operations, and other data modifications. The history retains the last 50 actions across the Data Editor, result sets, and related panels;
|
||||
- The SQL preview panel has been added to the Bind parameters/variables dialog in the SQL Editor, allowing users to review the SQL query with changed values on the fly;
|
||||
- The Tab key can now be used for autocompletion in the SQL editor in the same way as the Enter key;
|
||||
- Fixed an application freeze caused by fetch size canceling for large tables in the Result Set;
|
||||
- Fixed an issue where pressing the Delete key during inline cell editing incorrectly marked the entire row for deletion.
|
||||
- Navigator:
|
||||
- Added independent view preference settings for database connections at both the user and connection levels. Users can configure their preferred navigator view separately from connection-specific settings;
|
||||
- Added "Show children of tables" option to Navigator tree settings. When enabled, the navigator displays nested objects such as columns under tables and views. The setting applies globally across all connections and remains disabled by default;
|
||||
- Added the ability to rename connections via context menu in the Navigator Tree;
|
||||
- General:
|
||||
- Expanded pointer target areas for icons in the navigator, editors, and tabs to a minimum of pixels per accessibility standards. Improved interaction accuracy and reduced errors for users with motor control limitations;
|
||||
- The "Database Native" authentication type has been renamed to "Username/password" for all connectors;
|
||||
- ClickHouse: Fixed an issue with displaying arrays of UUID, IPv4/IPv6, and Map types.
|
||||
|
||||
### 25.3.3 2026-01-19
|
||||
|
||||
- For security reasons, session expiration leads to a complete logout for a user and clears all data from the screen;
|
||||
- Added "Use local formatting" setting to Data Editor. Users can choose how to display numbers and dates: using the OS locale, a custom locale, or keeping values unformatted. This formatting applies only to displayed values. Data in the database remains unchanged;
|
||||
- Added the shortcut Ctrl/Cmd + . to cancel operations in Data Editor;
|
||||
- CloudBeaver now supports long polling as a fallback when WebSockets are unavailable or blocked, ensuring reliable communication for metadata updates and SQL execution;
|
||||
- Fixed a keyboard navigation issue for panels to keep the focus inside;
|
||||
- Clickhouse: Spatial data support was added.
|
||||
|
||||
### 25.3.2 2026-01-05
|
||||
|
||||
- The ability to pin columns has been added to the Data Editor, allowing users to keep key columns (e.g., IDs, names) visible while scrolling through wide tables;
|
||||
- Added a new setting in the SQL Editor to highlight spaces, tabs, and other whitespace characters to help users read, debug, and maintain their scripts. It is turned off by default and can be configured in personal preferences;
|
||||
- Redesigned the connection configuration page. Form fields and sections have been reorganized to provide more space for input and reduce visual clutter;
|
||||
- Clickhouse: driver has been updated to version 0.9.5;
|
||||
- Oracle: Added a new "Set Username to OS_USER" option in the Misc section of Oracle connection settings. When turned on, the tool automatically uses the current database username as the operating system user identifier in the session metadata.
|
||||
|
||||
### 25.3.1 2025-12-22
|
||||
|
||||
- Implemented an ability to add parameters and variables to queries in the SQL Editor. This feature allows for the reuse of queries by changing parameters at the execution stage. It is enabled by default and can be configured in personal preferences;
|
||||
- Added support for mapping users to CloudBeaver teams based on LDAP memberOf group membership;
|
||||
- Added support for previous browser versions, but not older than three years.
|
||||
|
||||
### 25.3.0 2025-12-01
|
||||
|
||||
### Changes since 25.2.0:
|
||||
|
||||
- Administration:
|
||||
- Permission configuration for connections has been simplified, providing a clearer appearance and improved performance;
|
||||
- LDAP authentication now supports case-insensitive Distinguished Names (DNs), allowing login with mixed-case DNS.
|
||||
- Security:
|
||||
- Fixed a security issue in the Spatial Viewer for the HTML content to prevent a third-party script execution;
|
||||
- Fixed the critical vulnerability (CVE-2025-61927) in the Happy DOM library. The library was updated to version 20.0.2.
|
||||
- Enforced complete logout and screen data clearance upon session expiration;
|
||||
- Fixed the high vulnerability (CVE-2026-25639) in the axios library. The library was updated to version 1.13.5.
|
||||
- Administration:
|
||||
- Added support for mapping users to CloudBeaver teams based on LDAP memberOf group membership;
|
||||
- Change the User list settings in the Administration part to show both active and inactive users by default.
|
||||
- SQL Editor:
|
||||
- A confirmation prompt has been added to the SQL Editor to prevent accidental execution of potentially dangerous queries. You can customize this behavior in Preferences > SQL Editor;
|
||||
- A search field has been added to the catalog/schema selector on the main toolbar for easy navigation;
|
||||
- Alphabetical order has been added to the autocomplete values in the SQL Editor to simplify object search;
|
||||
- Added support for temporary tables in semantic analysis.
|
||||
- Added support for parameters and variables in queries. This feature allows queries to be reused by changing parameters at execution time. Enabled by default and configurable in personal preferences;
|
||||
- Added SQL preview to the Bind parameters/variables dialog to review queries with changed values on the fly;
|
||||
- Enabled Tab key for autocompletion in the SQL Editor alongside the Enter key;
|
||||
- Added a new setting in the SQL Editor to highlight spaces, tabs, and other whitespace characters to help users read, debug, and maintain their scripts. It is turned off by default and can be configured in personal preferences;
|
||||
- Dangerous query confirmation is now shown for all DROP statements, not just for tables.
|
||||
- Data Editor:
|
||||
- The ability to review the database query after applying filters and sorting in the Data Editor, Grouping panel, and Result sets has been added. The new Show SQL button is placed on the Filter panel;
|
||||
- The value panel in the Data Editor has been redesigned to give users more space for viewing and editing data. Tab buttons for different data type visualizations are now aligned on a single level, creating a cleaner layout;
|
||||
- Added visualization for column reordering in the Data Editor;
|
||||
- Fixed an issue in the Data Editor where resizing one column in a wide table caused previously resized columns to revert to their original width. Users can resize multiple columns as intended.
|
||||
- Added ability to automatically generate INSERT, SELECT, DELETE, and UPDATE statements for the selected values;
|
||||
- Added undo and redo functionality for cell edits, row operations, and other data modifications. Retains the last 50 actions across the Data Editor, result sets, and related panels;
|
||||
- Added "Use local formatting" setting. Users can choose how to display numbers and dates: using the OS locale, a custom locale, or keeping values unformatted. This formatting applies only to displayed values. Data in the database remains unchanged;
|
||||
- Added column pinning to keep key columns (e.g., IDs, names) visible while horizontal scrolling through wide tables;
|
||||
- Added status indicator icon in the top-left corner with tooltips explaining table editability. Indicates presence of primary keys, read-only connection settings, or read-only columns;
|
||||
- Added shortcut Ctrl/Cmd + . to cancel operations in Data Editor;
|
||||
- Fixed application freeze in canceling fetch size requests for large tables.
|
||||
- Navigator tree:
|
||||
- Added the ability to duplicate connection configuration in the project navigation tree. The "Clone connection" feature is available in the context menu;
|
||||
- Added the ability for users to configure the Simple or Advanced view in the Navigation tree for all connections or for each connection separately;
|
||||
- Added the ability to show table objects, such as columns or keys, in the Navigation tree. The setting is disabled by default and can be turned on in the Navigator settings panel;
|
||||
- Added the ability to rename connections via context menu in the Navigation Tree;
|
||||
- Added Connection Info tab to display basic information about the current connection for all users.
|
||||
- General:
|
||||
- A reorder tabs option has been added to the application, allowing users to set editor tabs in custom positions;
|
||||
- Keyboard navigation has been improved across the application. Additional shortcuts are available in the main info panel (top-right corner) to help discover them faster. Relevant keyboard shortcuts are also shown in the tooltips of Data Editor buttons;
|
||||
- Boolean values have been replaced with intuitive checkboxes instead of the true/false text in the metadata editor, making editing easier and faster;
|
||||
- The database navigator now automatically hides empty subfolders in shared projects, reducing visual clutter;
|
||||
- Databases and drivers:
|
||||
- ClickHouse: query canceling support has been added;
|
||||
- DuckDB: fixed an issue where catalog selection was lost upon disconnection;
|
||||
- PostgreSQL: fixed support for GIS data types;
|
||||
- SQL Server: the driver has been updated to version 13.2.1;
|
||||
- The "Show all databases" setting has been added on the connection page for MySQL and MariaDB.
|
||||
## Contribution
|
||||
As a community-driven open-source project, we warmly welcome contributions through GitHub pull requests.
|
||||
- Added support for long polling as a fallback when WebSockets are unavailable or blocked. Ensures reliable communication for metadata updates and SQL execution;
|
||||
- Extended browser support to versions up to three years old;
|
||||
- Redesigned the connection configuration page. Reorganized form fields and sections to provide more input space and reduce visual clutter;
|
||||
- Expanded pointer target areas for icons in the Navigator, editors, and tabs according to the accessibility standards;
|
||||
- Fixed a keyboard navigation issue for panels to keep the focus inside;
|
||||
- Renamed "Database Native" authentication type to "Username/password" in the connection dialog;
|
||||
- Fixed issue where the missing pg_dump utility caused errors during initialization or deployment updates when PostgreSQL was selected as the inner database.
|
||||
- Databases:
|
||||
- ClickHouse:
|
||||
- Updated driver to version 0.9.5
|
||||
- Added spatial data support
|
||||
- Fixed an issue with displaying arrays of UUID, IPv4/IPv6, and Map types
|
||||
- Fixed JSON data type reading
|
||||
- Resolved an issue with CTE expressions
|
||||
- DuckDB:
|
||||
- Updated driver to version 1.4.4.0;
|
||||
- Added support for the dollar-quoted string syntax for the SQL Editor
|
||||
- Oracle: Added a new "Set Username to OS_USER" option in the Misc section of Oracle connection settings. Automatically uses the current database username as the operating system user identifier in session metadata when enabled;
|
||||
- PostgreSQL: Added DDL display support for PostgreSQL policies.
|
||||
|
||||
[We are happy to reward](https://dbeaver.com/help-dbeaver/) our most active contributors every major sprint.
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Model
|
||||
Bundle-SymbolicName: io.cloudbeaver.model.cli;singleton:=true
|
||||
Bundle-Version: 1.0.5.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.6.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model.cli;visibility:=reexport
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.model.cli</artifactId>
|
||||
<version>1.0.5-SNAPSHOT</version>
|
||||
<version>1.0.6-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+5
-11
@@ -18,27 +18,24 @@ package io.cloudbeaver.model.cli;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.cli.*;
|
||||
import org.jkiss.dbeaver.model.cli.model.CommandLineAuthenticator;
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationCommandLine;
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
|
||||
import org.jkiss.dbeaver.model.cli.CLIContextImpl;
|
||||
import org.jkiss.dbeaver.model.cli.CLIRunMeta;
|
||||
import picocli.CommandLine;
|
||||
|
||||
public class CloudBeaverCommandLine extends ApplicationCommandLine<ApplicationInstanceController> {
|
||||
@Nullable
|
||||
private final CommandLineAuthenticator authenticator;
|
||||
private final CloudBeaverMixin mixin;
|
||||
|
||||
public CloudBeaverCommandLine(@Nullable CommandLineAuthenticator authenticator) {
|
||||
public CloudBeaverCommandLine() {
|
||||
super();
|
||||
this.authenticator = authenticator;
|
||||
this.mixin = new CloudBeaverMixin();
|
||||
}
|
||||
|
||||
public CloudBeaverCommandLine(
|
||||
@Nullable CommandLineAuthenticator authenticator,
|
||||
@NotNull CloudBeaverMixin mixin
|
||||
) {
|
||||
super();
|
||||
this.authenticator = authenticator;
|
||||
this.mixin = mixin;
|
||||
}
|
||||
|
||||
@@ -48,9 +45,6 @@ public class CloudBeaverCommandLine extends ApplicationCommandLine<ApplicationIn
|
||||
@NotNull CLIContextImpl context,
|
||||
@NotNull CLIRunMeta runMeta
|
||||
) {
|
||||
if (authenticator != null) {
|
||||
context.setContextParameter(CLIConstants.CONTEXT_PARAM_AUTHENTICATOR, authenticator);
|
||||
}
|
||||
return new CloudBeaverTopLevelCommand(applicationInstanceController, context, runMeta);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class CloudBeaverInstanceServer extends ApplicationInstanceServer<Applica
|
||||
|
||||
public CloudBeaverInstanceServer() throws IOException {
|
||||
super(ApplicationInstanceController.class);
|
||||
this.commandLine = new CloudBeaverCommandLine(null);
|
||||
this.commandLine = new CloudBeaverCommandLine();
|
||||
}
|
||||
|
||||
public CloudBeaverInstanceServer(@NotNull CloudBeaverCommandLine commandLine) throws IOException {
|
||||
|
||||
+3
-1
@@ -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.
|
||||
@@ -16,12 +16,14 @@
|
||||
*/
|
||||
package io.cloudbeaver.model.cli;
|
||||
|
||||
import org.jkiss.dbeaver.model.cli.model.NonExecutableOption;
|
||||
import picocli.CommandLine;
|
||||
|
||||
/**
|
||||
* CloudBeaver specific mixin options
|
||||
*/
|
||||
public class CloudBeaverMixin {
|
||||
@NonExecutableOption
|
||||
@CommandLine.Option(
|
||||
names = {"-web-config"},
|
||||
arity = "1",
|
||||
|
||||
+2
@@ -22,10 +22,12 @@ import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
|
||||
import org.jkiss.dbeaver.model.cli.CLIContextImpl;
|
||||
import org.jkiss.dbeaver.model.cli.CLIRunMeta;
|
||||
import org.jkiss.dbeaver.model.cli.command.AbstractTopLevelCommand;
|
||||
import org.jkiss.dbeaver.model.cli.model.NonExecutableOption;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(name = "cbvr", description = "CloudBeaver commands")
|
||||
public class CloudBeaverTopLevelCommand extends AbstractTopLevelCommand {
|
||||
@NonExecutableOption
|
||||
@CommandLine.Option(
|
||||
names = {NOSPASH_OPTION},
|
||||
hidden = true,
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Model
|
||||
Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true
|
||||
Bundle-Version: 1.0.96.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.97.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.model</artifactId>
|
||||
<version>1.0.96-SNAPSHOT</version>
|
||||
<version>1.0.97-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+6
-5
@@ -31,8 +31,6 @@ import org.jkiss.dbeaver.model.rm.RMProject;
|
||||
import org.jkiss.dbeaver.model.rm.RMResource;
|
||||
import org.jkiss.dbeaver.model.rm.RMResourceType;
|
||||
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
|
||||
import org.jkiss.dbeaver.registry.ResourceTypeDescriptor;
|
||||
import org.jkiss.dbeaver.registry.ResourceTypeRegistry;
|
||||
import org.jkiss.utils.ArrayUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
@@ -44,7 +42,7 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode {
|
||||
|
||||
private final RMResource resource;
|
||||
|
||||
DBNResourceManagerResource(DBNNode parentNode, RMResource resource) {
|
||||
DBNResourceManagerResource(@NotNull DBNNode parentNode, @NotNull RMResource resource) {
|
||||
super(parentNode);
|
||||
this.resource = resource;
|
||||
}
|
||||
@@ -71,6 +69,7 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode {
|
||||
@Override
|
||||
public DBPImage getNodeIcon() {
|
||||
if (resource.isFolder()) {
|
||||
/*
|
||||
if (getParentNode() instanceof DBNResourceManagerResource) {
|
||||
return DBIcon.TREE_FOLDER;
|
||||
}
|
||||
@@ -82,11 +81,12 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode {
|
||||
if (folderResType != null) {
|
||||
return folderResType.getFolderIcon();
|
||||
}
|
||||
*/
|
||||
return DBIcon.TREE_FOLDER;
|
||||
} else {
|
||||
var fileExtension = IOUtils.getFileExtension(getNodeDisplayName());
|
||||
if (!CommonUtils.isEmpty(fileExtension)) {
|
||||
RMProject project = getProjectNode();
|
||||
RMProject project = getRmProject();
|
||||
if (project != null) {
|
||||
for (RMResourceType rt : project.getResourceTypes()) {
|
||||
if (ArrayUtils.contains(rt.getFileExtensions(), fileExtension)) {
|
||||
@@ -99,7 +99,8 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode {
|
||||
}
|
||||
}
|
||||
|
||||
private RMProject getProjectNode() {
|
||||
@Nullable
|
||||
public RMProject getRmProject() {
|
||||
for (DBNNode node = this; node != null; node = node.getParentNode()) {
|
||||
if (node instanceof DBNResourceManagerProject) {
|
||||
return ((DBNResourceManagerProject) node).getProject();
|
||||
|
||||
+8
-8
@@ -102,7 +102,7 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void addSessionError(Throwable exception);
|
||||
public abstract void addSessionError(@NotNull Throwable exception);
|
||||
|
||||
public void addEventHandler(@NotNull CBWebSessionEventHandler handler) {
|
||||
synchronized (sessionEventHandlers) {
|
||||
@@ -116,11 +116,11 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
|
||||
public boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
|
||||
return userContext.refresh(smAuthInfo);
|
||||
}
|
||||
|
||||
public synchronized void refreshUserData() {
|
||||
public void refreshUserData() {
|
||||
try {
|
||||
userContext.refreshPermissions();
|
||||
if (userContext.isAuthorizedInSecurityManager()) {
|
||||
@@ -150,7 +150,7 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
return workspace.getAuthContext();
|
||||
}
|
||||
|
||||
protected void clearSessionContext() {
|
||||
protected synchronized void clearSessionContext() {
|
||||
this.workspace.getAuthContext().clear();
|
||||
this.workspace.getAuthContext().addSession(this);
|
||||
}
|
||||
@@ -176,12 +176,12 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
return lastAccessTime;
|
||||
}
|
||||
|
||||
public synchronized void touchSession() {
|
||||
public void touchSession() {
|
||||
this.lastAccessTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized WebUserContext getUserContext() {
|
||||
public WebUserContext getUserContext() {
|
||||
return userContext;
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
this.eventsFilter = eventsFilter;
|
||||
}
|
||||
|
||||
public boolean isProjectAccessible(String projectId) {
|
||||
public boolean isProjectAccessible(@NotNull String projectId) {
|
||||
return userContext.getAccessibleProjectIds().contains(projectId);
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
userContext.getAccessibleProjectIds().remove(projectId);
|
||||
}
|
||||
|
||||
public abstract void addSessionMessage(WebServerMessage message);
|
||||
public abstract void addSessionMessage(@NotNull WebServerMessage message);
|
||||
|
||||
@Property
|
||||
public boolean isValid() {
|
||||
|
||||
+2
-2
@@ -35,12 +35,12 @@ public class WebHeadlessSession extends BaseWebSession {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSessionError(Throwable exception) {
|
||||
public void addSessionError(@NotNull Throwable exception) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSessionMessage(WebServerMessage message) {
|
||||
public void addSessionMessage(@NotNull WebServerMessage message) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
+84
-48
@@ -64,6 +64,8 @@ import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.stream.Collectors;
|
||||
/**
|
||||
* Web session.
|
||||
@@ -99,6 +101,7 @@ public class WebSession extends BaseWebSession
|
||||
private final List<WebAuthInfo> authTokens = new ArrayList<>();
|
||||
|
||||
private DBNModel navigatorModel;
|
||||
private final ReadWriteLock navigatorModelLock = new ReentrantReadWriteLock(true);
|
||||
private final DBRProgressMonitor progressMonitor = new SessionProgressMonitor();
|
||||
private final Map<String, DBWSessionHandler<WebSession>> sessionHandlers;
|
||||
private final WebDataSourceConnectEventListener connectListener = new WebDataSourceConnectEventListener(this);
|
||||
@@ -185,11 +188,12 @@ public class WebSession extends BaseWebSession
|
||||
this.cacheExpired = cacheExpired;
|
||||
}
|
||||
|
||||
public synchronized WebUser getUser() {
|
||||
public WebUser getUser() {
|
||||
return this.userContext.getUser();
|
||||
}
|
||||
|
||||
public synchronized Map<String, String> getUserMetaParameters() {
|
||||
@NotNull
|
||||
public Map<String, String> getUserMetaParameters() {
|
||||
var user = getUser();
|
||||
if (user == null) {
|
||||
return Map.of();
|
||||
@@ -201,20 +205,20 @@ public class WebSession extends BaseWebSession
|
||||
return allMetaParams;
|
||||
}
|
||||
|
||||
public synchronized String getUserId() {
|
||||
public String getUserId() {
|
||||
return userContext.getUserId();
|
||||
}
|
||||
|
||||
public synchronized boolean hasPermission(String perm) {
|
||||
return getSessionPermissions().contains(DBWConstants.PERMISSION_ADMIN) ||
|
||||
getSessionPermissions().contains(perm);
|
||||
Set<String> sessionPermissions = getSessionPermissions();
|
||||
return sessionPermissions.contains(DBWConstants.PERMISSION_ADMIN) || sessionPermissions.contains(perm);
|
||||
}
|
||||
|
||||
public synchronized boolean isAuthorizedInSecurityManager() {
|
||||
public boolean isAuthorizedInSecurityManager() {
|
||||
return userContext.isAuthorizedInSecurityManager();
|
||||
}
|
||||
|
||||
public synchronized Set<String> getSessionPermissions() {
|
||||
public Set<String> getSessionPermissions() {
|
||||
if (userContext.getUserPermissions() == null) {
|
||||
refreshSessionAuth();
|
||||
}
|
||||
@@ -222,12 +226,12 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized SMController getSecurityController() {
|
||||
public SMController getSecurityController() {
|
||||
return userContext.getSecurityController();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized SMAdminController getAdminSecurityController() throws DBException {
|
||||
public SMAdminController getAdminSecurityController() throws DBException {
|
||||
if (!hasPermission(DBWConstants.PERMISSION_ADMIN)) {
|
||||
throw new DBException("Admin permissions required");
|
||||
}
|
||||
@@ -235,17 +239,17 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized RMController getRmController() {
|
||||
public RMController getRmController() {
|
||||
return userContext.getRmController();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized DBFileController getFileController() {
|
||||
public DBFileController getFileController() {
|
||||
return userContext.getFileController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void refreshUserData() {
|
||||
public void refreshUserData() {
|
||||
super.refreshUserData();
|
||||
refreshSessionAuth();
|
||||
|
||||
@@ -253,7 +257,7 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
|
||||
// Note: for admin use only
|
||||
public synchronized void resetUserState(boolean needResetUserCache) throws DBException {
|
||||
public void resetUserState(boolean needResetUserCache) throws DBException {
|
||||
clearAuthTokens();
|
||||
if (needResetUserCache) {
|
||||
try {
|
||||
@@ -267,7 +271,7 @@ public class WebSession extends BaseWebSession
|
||||
clearSessionContext();
|
||||
}
|
||||
|
||||
public synchronized void resetUserState() throws DBException {
|
||||
public void resetUserState() throws DBException {
|
||||
resetUserState(true);
|
||||
}
|
||||
|
||||
@@ -286,21 +290,25 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
|
||||
private void initNavigatorModel() {
|
||||
navigatorModelLock.writeLock().lock();
|
||||
try {
|
||||
// Cleanup current data
|
||||
if (this.navigatorModel != null) {
|
||||
this.navigatorModel.dispose();
|
||||
this.navigatorModel = null;
|
||||
}
|
||||
this.globalProject = null;
|
||||
|
||||
// Cleanup current data
|
||||
if (this.navigatorModel != null) {
|
||||
this.navigatorModel.dispose();
|
||||
this.navigatorModel = null;
|
||||
loadProjects();
|
||||
|
||||
this.navigatorModel = new DBNModel(DBWorkbench.getPlatform(), getWorkspace().getProjects());
|
||||
this.navigatorModel.setModelAuthContext(getWorkspace().getAuthContext());
|
||||
this.navigatorModel.initialize();
|
||||
|
||||
this.locale = Locale.getDefault().getLanguage();
|
||||
} finally {
|
||||
navigatorModelLock.writeLock().unlock();
|
||||
}
|
||||
this.globalProject = null;
|
||||
|
||||
loadProjects();
|
||||
|
||||
this.navigatorModel = new DBNModel(DBWorkbench.getPlatform(), getWorkspace().getProjects());
|
||||
this.navigatorModel.setModelAuthContext(getWorkspace().getAuthContext());
|
||||
this.navigatorModel.initialize();
|
||||
|
||||
this.locale = Locale.getDefault().getLanguage();
|
||||
}
|
||||
|
||||
private void loadProjects() {
|
||||
@@ -368,11 +376,16 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
|
||||
private void resetNavigationModel() {
|
||||
getWorkspace().getProjects().forEach(WebSessionProjectImpl::dispose);
|
||||
navigatorModelLock.writeLock().lock();
|
||||
try {
|
||||
getWorkspace().getProjects().forEach(WebSessionProjectImpl::dispose);
|
||||
|
||||
if (this.navigatorModel != null) {
|
||||
this.navigatorModel.dispose();
|
||||
this.navigatorModel = null;
|
||||
if (this.navigatorModel != null) {
|
||||
this.navigatorModel.dispose();
|
||||
this.navigatorModel = null;
|
||||
}
|
||||
} finally {
|
||||
navigatorModelLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,15 +428,25 @@ public class WebSession extends BaseWebSession
|
||||
|
||||
@Nullable
|
||||
public DBNModel getNavigatorModel() {
|
||||
return navigatorModel;
|
||||
navigatorModelLock.readLock().lock();
|
||||
try {
|
||||
return navigatorModel;
|
||||
} finally {
|
||||
navigatorModelLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DBNModel getNavigatorModelOrThrow() throws DBWebException {
|
||||
if (navigatorModel != null) {
|
||||
return navigatorModel;
|
||||
navigatorModelLock.readLock().lock();
|
||||
try {
|
||||
if (navigatorModel != null) {
|
||||
return navigatorModel;
|
||||
}
|
||||
throw new DBWebException("Navigator model is not found in session");
|
||||
} finally {
|
||||
navigatorModelLock.readLock().unlock();
|
||||
}
|
||||
throw new DBWebException("Navigator model is not found in session");
|
||||
}
|
||||
/**
|
||||
* Returns and clears progress messages
|
||||
@@ -437,7 +460,7 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void updateInfo(boolean isOldHttpSessionUsed) {
|
||||
public void updateInfo(boolean isOldHttpSessionUsed) {
|
||||
log.trace("Update session lifetime " + getSessionId() + " for user " + getUserId());
|
||||
touchSession();
|
||||
if (isOldHttpSessionUsed) {
|
||||
@@ -501,6 +524,7 @@ public class WebSession extends BaseWebSession
|
||||
super.close(clearTokens, sendSessionExpiredEvent);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<WebAuthInfo> clearAuthTokens() throws DBException {
|
||||
ArrayList<WebAuthInfo> tokensCopy;
|
||||
synchronized (authTokens) {
|
||||
@@ -630,11 +654,11 @@ public class WebSession extends BaseWebSession
|
||||
return asyncTask;
|
||||
}
|
||||
|
||||
public void addSessionError(Throwable exception) {
|
||||
public void addSessionError(@NotNull Throwable exception) {
|
||||
addSessionMessage(new WebServerMessage(exception));
|
||||
}
|
||||
|
||||
public void addSessionMessage(WebServerMessage message) {
|
||||
public void addSessionMessage(@NotNull WebServerMessage message) {
|
||||
synchronized (sessionMessages) {
|
||||
sessionMessages.add(message);
|
||||
}
|
||||
@@ -645,14 +669,15 @@ public class WebSession extends BaseWebSession
|
||||
message.getMessage()));
|
||||
}
|
||||
|
||||
public void addInfoMessage(String message) {
|
||||
public void addInfoMessage(@NotNull String message) {
|
||||
addSessionMessage(new WebServerMessage(MessageType.INFO, message));
|
||||
}
|
||||
|
||||
public void addWarningMessage(String message) {
|
||||
public void addWarningMessage(@NotNull String message) {
|
||||
addSessionMessage(new WebServerMessage(MessageType.WARNING, message));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<WebServerMessage> readLog(Integer maxEntries, Boolean clearLog) {
|
||||
synchronized (sessionMessages) {
|
||||
List<WebServerMessage> messages = new ArrayList<>();
|
||||
@@ -700,7 +725,7 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
public List<WebAuthInfo> getAllAuthInfo() {
|
||||
synchronized (authTokens) {
|
||||
return new ArrayList<>(authTokens);
|
||||
@@ -752,7 +777,7 @@ public class WebSession extends BaseWebSession
|
||||
});
|
||||
}
|
||||
|
||||
private void removeAuthInfo(WebAuthInfo oldAuthInfo) {
|
||||
private void removeAuthInfo(@NotNull WebAuthInfo oldAuthInfo) {
|
||||
oldAuthInfo.closeAuth();
|
||||
synchronized (authTokens) {
|
||||
authTokens.remove(oldAuthInfo);
|
||||
@@ -778,6 +803,7 @@ public class WebSession extends BaseWebSession
|
||||
return oldInfo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<DBACredentialsProvider> getContextCredentialsProviders() {
|
||||
return getAdapters(DBACredentialsProvider.class);
|
||||
}
|
||||
@@ -865,11 +891,11 @@ public class WebSession extends BaseWebSession
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public synchronized void resetAuthToken() throws DBException {
|
||||
public void resetAuthToken() throws DBException {
|
||||
this.userContext.reset();
|
||||
}
|
||||
|
||||
public synchronized boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
|
||||
public boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
|
||||
boolean contextChanged = super.updateSMSession(smAuthInfo);
|
||||
if (contextChanged) {
|
||||
refreshUserData();
|
||||
@@ -919,8 +945,13 @@ public class WebSession extends BaseWebSession
|
||||
*/
|
||||
public void addSessionProject(@NotNull WebSessionProjectImpl project) {
|
||||
getWorkspace().addProject(project);
|
||||
if (navigatorModel != null) {
|
||||
navigatorModel.getRoot().addProject(project, false);
|
||||
navigatorModelLock.readLock().lock();
|
||||
try {
|
||||
if (navigatorModel != null) {
|
||||
navigatorModel.getRoot().addProject(project, false);
|
||||
}
|
||||
} finally {
|
||||
navigatorModelLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,8 +967,13 @@ public class WebSession extends BaseWebSession
|
||||
project.dispose();
|
||||
|
||||
getWorkspace().removeProject(project);
|
||||
if (navigatorModel != null) {
|
||||
navigatorModel.getRoot().removeProject(project);
|
||||
navigatorModelLock.readLock().lock();
|
||||
try {
|
||||
if (navigatorModel != null) {
|
||||
navigatorModel.getRoot().removeProject(project);
|
||||
}
|
||||
} finally {
|
||||
navigatorModelLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-16
@@ -53,9 +53,9 @@ public class WebUserContext implements SMCredentialsProvider {
|
||||
private final ServletApplication application;
|
||||
private final DBPWorkspace workspace;
|
||||
|
||||
private WebUser user;
|
||||
private Set<String> userPermissions;
|
||||
private SMCredentials smCredentials = null;
|
||||
private volatile WebUser user;
|
||||
private volatile Set<String> userPermissions;
|
||||
private volatile SMCredentials smCredentials = null;
|
||||
private String smSessionId;
|
||||
private String refreshToken;
|
||||
|
||||
@@ -87,7 +87,7 @@ public class WebUserContext implements SMCredentialsProvider {
|
||||
* @return - true if context changed
|
||||
* @throws DBException - if user already authorized and new token come from another user
|
||||
*/
|
||||
public synchronized boolean refresh(@NotNull SMAuthInfo smAuthInfo) throws DBException {
|
||||
public boolean refresh(@NotNull SMAuthInfo smAuthInfo) throws DBException {
|
||||
if (smAuthInfo.getAuthPermissions() == null && !isAuthorizedInSecurityManager()) {
|
||||
throw new DBCException("Required information about session permissions is missing");
|
||||
}
|
||||
@@ -175,56 +175,56 @@ public class WebUserContext implements SMCredentialsProvider {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized SMController getSecurityController() {
|
||||
public SMController getSecurityController() {
|
||||
return securityController;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public synchronized SMCredentials getActiveUserCredentials() {
|
||||
public SMCredentials getActiveUserCredentials() {
|
||||
return smCredentials;
|
||||
}
|
||||
|
||||
public synchronized boolean isAuthorizedInSecurityManager() {
|
||||
public boolean isAuthorizedInSecurityManager() {
|
||||
return smCredentials != null;
|
||||
}
|
||||
|
||||
public synchronized boolean isNonAnonymousUserAuthorizedInSM() {
|
||||
public boolean isNonAnonymousUserAuthorizedInSM() {
|
||||
return isAuthorizedInSecurityManager() && getUser() != null;
|
||||
}
|
||||
|
||||
public synchronized boolean isAnonymousUserAuthorizedInSM() {
|
||||
public boolean isAnonymousUserAuthorizedInSM() {
|
||||
return isAuthorizedInSecurityManager() && getUser() == null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized WebUser getUser() {
|
||||
public WebUser getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized String getUserId() {
|
||||
public String getUserId() {
|
||||
return user == null ? null : user.getUserId();
|
||||
}
|
||||
|
||||
protected synchronized void setUser(@Nullable WebUser user) {
|
||||
protected void setUser(@Nullable WebUser user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized SMAdminController getAdminSecurityController() {
|
||||
public SMAdminController getAdminSecurityController() {
|
||||
return adminSecurityController;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized Set<String> getUserPermissions() {
|
||||
public Set<String> getUserPermissions() {
|
||||
return userPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* reread the current user's permissions
|
||||
*/
|
||||
public synchronized void refreshPermissions() throws DBException {
|
||||
public void refreshPermissions() throws DBException {
|
||||
if (isAuthorizedInSecurityManager()) {
|
||||
log.debug("refresh permissions " + getUserId() + " " + getSmSessionId());
|
||||
setUserPermissions(securityController.getTokenPermissions().getPermissions());
|
||||
@@ -246,7 +246,7 @@ public class WebUserContext implements SMCredentialsProvider {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public synchronized String getSmSessionId() {
|
||||
public String getSmSessionId() {
|
||||
return smSessionId;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,49 +28,34 @@ public class CBConstants {
|
||||
public static final String RUNTIME_PRODUCT_CONFIG_FILE_NAME = ".product.runtime.conf";
|
||||
public static final String AUTO_CONFIG_FILE_NAME = ".cloudbeaver.auto.conf";
|
||||
|
||||
public static final String PARAM_SERVER_CONFIGURATION = "server";
|
||||
public static final String PARAM_SERVER_PORT = "serverPort";
|
||||
public static final String PARAM_SERVER_HOST = "serverHost";
|
||||
public static final String PARAM_SERVER_NAME = "serverName";
|
||||
public static final String PARAM_FORCE_HTTPS = "forceHttps";
|
||||
public static final String PARAM_BIND_SESSION_TO_IP = "bindSessionToIp";
|
||||
public static final String PARAM_SUPPORTED_HOSTS = "supportedHosts";
|
||||
public static final String PARAM_SSL_CONFIGURATION_PATH = "sslConfigurationPath";
|
||||
public static final String PARAM_CONTENT_ROOT = "contentRoot";
|
||||
public static final String PARAM_ROOT_URI = "rootURI";
|
||||
public static final String PARAM_SERVICES_URI = "serviceURI";
|
||||
public static final String PARAM_DRIVERS_LOCATION = "driversLocation";
|
||||
public static final String PARAM_WORKSPACE_LOCATION = "workspaceLocation";
|
||||
//legacy path to a product.conf file
|
||||
@Deprecated
|
||||
public static final String PARAM_PRODUCT_CONFIGURATION = "productConfiguration";
|
||||
public static final String PARAM_PRODUCT_SETTINGS = "productSettings";
|
||||
public static final String PARAM_EXTERNAL_PROPERTIES = "externalProperties";
|
||||
public static final String PARAM_STATIC_CONTENT = "staticContent";
|
||||
public static final String PARAM_RESOURCE_QUOTAS = "resourceQuotas";
|
||||
public static final String PARAM_RESOURCE_MANAGER_ENABLED = "resourceManagerEnabled";
|
||||
public static final String PARAM_SECRET_MANAGER_ENABLED = "secretManagerEnabled";
|
||||
public static final String PARAM_SHOW_READ_ONLY_CONN_INFO = "showReadOnlyConnectionInfo";
|
||||
public static final String PARAM_CONN_GRANT_ANON_ACCESS = "grantConnectionsAccessToAnonymousTeam";
|
||||
public static final String PARAM_AUTH_PROVIDERS = "authConfiguration";
|
||||
public static final String PARAM_DB_CONFIGURATION = "database";
|
||||
|
||||
public static final String PARAM_SESSION_EXPIRE_PERIOD = "expireSessionAfterPeriod";
|
||||
|
||||
public static final String PARAM_DEVEL_MODE = "develMode";
|
||||
public static final String PARAM_SECURITY_MANAGER = "enableSecurityManager";
|
||||
public static final String PARAM_SM_CONFIGURATION = "sm";
|
||||
public static final String PARAM_PASSWORD_POLICY_CONFIGURATION = "passwordPolicy";
|
||||
|
||||
public static final int DEFAULT_SERVER_PORT = 8080;
|
||||
//public static final String DEFAULT_SERVER_NAME = "CloudBeaver Web Server";
|
||||
|
||||
public static final String DEFAULT_CONTENT_ROOT = "/var/www/cloudbeaver";
|
||||
public static final String DEFAULT_ROOT_URI = "/";
|
||||
public static final String DEFAULT_SERVICES_URI = "/api/";
|
||||
|
||||
public static final String DEFAULT_DEPLOY_LOCATION = "/opt/cloudbeaver";
|
||||
public static final String DEFAULT_DRIVERS_LOCATION = DEFAULT_DEPLOY_LOCATION + "/drivers";
|
||||
public static final String DEFAULT_WORKSPACE_LOCATION = DEFAULT_DEPLOY_LOCATION + "/workspace";
|
||||
public static final String DEFAULT_PRODUCT_CONFIGURATION = "conf/product.conf";
|
||||
public static final String DEFAULT_DATASOURCE_PERMISSIONS_CONFIGURATION = "data-sources-permissions.json";
|
||||
public static final String DEFAULT_ADMIN_NAME = "cbadmin";
|
||||
@@ -83,14 +68,11 @@ public class CBConstants {
|
||||
public static final String VAR_HOST_DOCKER_INTERNAL = "host.docker.internal";
|
||||
|
||||
public static final String VAR_AUTO_CB_SERVER_NAME = "CB_SERVER_NAME";
|
||||
public static final String VAR_AUTO_CB_SERVER_URL = "CB_SERVER_URL";
|
||||
public static final String VAR_AUTO_CB_ADMIN_NAME = "CB_ADMIN_NAME";
|
||||
public static final String VAR_AUTO_CB_ADMIN_PASSWORD = "CB_ADMIN_PASSWORD";
|
||||
|
||||
public static final String CB_SESSION_COOKIE_NAME = "cb-session-id";
|
||||
|
||||
public static final String APPLICATION_JSON = "application/json";
|
||||
|
||||
public static final String ADMIN_AUTO_GRANT = "auto-grant";
|
||||
public static final String HOST_LOCALHOST = "localhost";
|
||||
public static final String HOST_127_0_0_1 = "127.0.0.1";
|
||||
|
||||
+13
-4
@@ -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.
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.data.DBDAttributeConstraint;
|
||||
import org.jkiss.dbeaver.model.gis.DBGeometry;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
@@ -27,6 +28,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class WebSQLDataFilterConstraint {
|
||||
|
||||
private String attributeName;
|
||||
private Integer attributePosition;
|
||||
private Integer orderPosition;
|
||||
private Boolean orderAsc;
|
||||
@@ -38,8 +40,9 @@ public class WebSQLDataFilterConstraint {
|
||||
private WebSQLDataFilterConstraint() {
|
||||
}
|
||||
|
||||
public WebSQLDataFilterConstraint(Map<String, Object> map) {
|
||||
this.attributePosition = CommonUtils.toInt(map.get("attributePosition"));
|
||||
public WebSQLDataFilterConstraint(@NotNull Map<String, Object> map) {
|
||||
this.attributeName = CommonUtils.toString(map.get("attributeName"));
|
||||
this.attributePosition = CommonUtils.toInteger(map.get("attributePosition"), null);
|
||||
this.orderPosition = map.containsKey("orderPosition") ?
|
||||
CommonUtils.toInt(map.get("orderPosition")) + 1 : // Use position + 1 because 0 means no ordering (because of legacy compatibility)
|
||||
null;
|
||||
@@ -64,8 +67,10 @@ public class WebSQLDataFilterConstraint {
|
||||
|
||||
}
|
||||
|
||||
public static WebSQLDataFilterConstraint from(DBDAttributeConstraint constraint) {
|
||||
@NotNull
|
||||
public static WebSQLDataFilterConstraint from(@NotNull DBDAttributeConstraint constraint) {
|
||||
var webConstraint = new WebSQLDataFilterConstraint();
|
||||
webConstraint.attributeName = constraint.getAttributeName();
|
||||
webConstraint.orderPosition = constraint.getOrderPosition();
|
||||
webConstraint.orderAsc = !constraint.isOrderDescending();
|
||||
webConstraint.criteria = constraint.getCriteria();
|
||||
@@ -76,6 +81,10 @@ public class WebSQLDataFilterConstraint {
|
||||
return webConstraint;
|
||||
}
|
||||
|
||||
public String getAttributeName() {
|
||||
return attributeName;
|
||||
}
|
||||
|
||||
public Integer getAttributePosition() {
|
||||
return attributePosition;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
|
||||
import java.net.CookieManager;
|
||||
import java.net.URI;
|
||||
@@ -127,7 +128,7 @@ public class WebGQLClient {
|
||||
.uri(URI.create(apiUrl))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(makeGQLRequest(query, variables)))
|
||||
.setHeader("TE-Client-Version", GeneralUtils.getMajorVersion())
|
||||
.header("Content-Type", "application/json");
|
||||
.header(HttpConstants.HEADER_CONTENT_TYPE, HttpConstants.CONTENT_TYPE_JSON);
|
||||
if (!requestHeaders.isEmpty()) {
|
||||
requestHeaders.forEach(requestBuilder::header);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -47,6 +47,8 @@ public class ServletAppUtils {
|
||||
private static final String HEADER_REFERER = "Referer";
|
||||
|
||||
private static final String HEADER_FORWARDED_SCHEME = "X-Forwarded-Scheme";
|
||||
private static final String HEADER_FORWARDED_PROTO = "X-Forwarded-Proto";
|
||||
private static final String HEADER_FORWARDED_PORT = "X-Forwarded-Port";
|
||||
private static final String HEADER_FORWARDED_HOST = "X-Forwarded-Host";
|
||||
private static final Set<Integer> DEFAULT_PORTS = Set.of(
|
||||
80,
|
||||
@@ -294,19 +296,37 @@ public class ServletAppUtils {
|
||||
@NotNull
|
||||
public static String getOriginFromRequest(@NotNull HttpServletRequest request) {
|
||||
String origin = request.getHeader(HEADER_ORIGIN);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Origin header: " + origin);
|
||||
}
|
||||
if (CommonUtils.isEmpty(origin)) {
|
||||
origin = request.getHeader(HEADER_X_ORIGIN);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("X-Origin header: " + origin);
|
||||
}
|
||||
}
|
||||
if (CommonUtils.isEmpty(origin)) {
|
||||
origin = request.getHeader(HEADER_REFERER);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Referer header: " + origin);
|
||||
}
|
||||
}
|
||||
String forwardedScheme = request.getHeader(HEADER_FORWARDED_SCHEME);
|
||||
if (CommonUtils.isEmpty(forwardedScheme)) {
|
||||
forwardedScheme = request.getHeader(HEADER_FORWARDED_PROTO);
|
||||
}
|
||||
String forwardedHost = request.getHeader(HEADER_FORWARDED_HOST);
|
||||
if (CommonUtils.isNotEmpty(forwardedScheme) && CommonUtils.isNotEmpty(forwardedHost)) {
|
||||
origin = forwardedScheme + "://" + forwardedHost;
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("forwarded origin: " + origin);
|
||||
}
|
||||
}
|
||||
if (CommonUtils.isEmpty(origin)) {
|
||||
URI requestUrl = URI.create(request.getRequestURL().toString());
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Request URL: " + requestUrl);
|
||||
}
|
||||
origin = getRootUrlFromUri(requestUrl) + "/";
|
||||
}
|
||||
origin = removeSideSlashes(origin);
|
||||
@@ -318,7 +338,15 @@ public class ServletAppUtils {
|
||||
|
||||
origin = removeSideSlashes(origin);
|
||||
URI uri = URI.create(origin);
|
||||
if (DEFAULT_PORTS.contains(uri.getPort())) {
|
||||
int port = uri.getPort();
|
||||
if (CommonUtils.isNotEmpty(request.getHeader(HEADER_FORWARDED_PORT))) {
|
||||
try {
|
||||
port = Integer.parseInt(request.getHeader(HEADER_FORWARDED_PORT));
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("Failed to parse port from header: " + request.getHeader(HEADER_FORWARDED_PORT), e);
|
||||
}
|
||||
}
|
||||
if (DEFAULT_PORTS.contains(port)) {
|
||||
try {
|
||||
origin = new URI(
|
||||
uri.getScheme(),
|
||||
@@ -333,6 +361,9 @@ public class ServletAppUtils {
|
||||
log.error("Failed to create URI without port", e);
|
||||
}
|
||||
}
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Origin URI: " + origin);
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Community Product
|
||||
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
|
||||
Bundle-Version: 26.0.0.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 26.0.1.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.product.ce</artifactId>
|
||||
<version>26.0.0-SNAPSHOT</version>
|
||||
<version>26.0.1-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Cloudbeaver Base JDBC drivers
|
||||
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
|
||||
Bundle-Version: 1.0.141.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.142.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Automatic-Module-Name: io.cloudbeaver.resources.drivers.base
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.resources.drivers.base</artifactId>
|
||||
<version>1.0.141-SNAPSHOT</version>
|
||||
<version>1.0.142-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: CloudBeaver Community
|
||||
Bundle-SymbolicName: io.cloudbeaver.server.ce;singleton:=true
|
||||
Bundle-Version: 26.0.0.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 26.0.1.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server.ce</artifactId>
|
||||
<version>26.0.0-SNAPSHOT</version>
|
||||
<version>26.0.1-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+14
-3
@@ -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.
|
||||
@@ -33,6 +33,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public class RequestHostFilter implements Filter {
|
||||
private static final Log log = Log.getLog(RequestHostFilter.class);
|
||||
|
||||
@@ -74,9 +75,19 @@ public class RequestHostFilter implements Filter {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
boolean isIpAddress = InetAddresses.isInetAddress(originUri.getHost());
|
||||
|
||||
if (CommonUtils.isNotEmpty(originUri.getHost())) {
|
||||
requestAllowed = InetAddresses.isInetAddress(originUri.getHost());
|
||||
} else {
|
||||
log.debug(
|
||||
"Request origin host is null, request URI - " + originUri +
|
||||
", request path - " + httpRequest.getServletPath() +
|
||||
", request url - " + httpRequest.getRequestURL()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
String servletPath = httpRequest.getServletPath();
|
||||
requestAllowed = isIpAddress;
|
||||
if (!requestAllowed) {
|
||||
if (CommonUtils.isNotEmpty(servletPath)) {
|
||||
for (String excludedPath : excludedPaths) {
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Server
|
||||
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
|
||||
Bundle-Version: 26.0.0.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 26.0.1.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.eclipse.core.runtime;visibility:=reexport,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server</artifactId>
|
||||
<version>26.0.0-SNAPSHOT</version>
|
||||
<version>26.0.1-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -825,7 +825,7 @@ extend type Mutation {
|
||||
testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo!
|
||||
|
||||
"Test network handler connectivity"
|
||||
testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo!
|
||||
testNetworkHandler(projectId: ID, connectionId: ID, config: NetworkHandlerConfigInput! ): NetworkEndpointInfo!
|
||||
|
||||
"Initiate database connection. User credentials may be (optionally) override default settings."
|
||||
initConnection(
|
||||
|
||||
@@ -59,6 +59,7 @@ enum CBServerEventId {
|
||||
cb_database_output_log_updated,
|
||||
cb_ai_chat_message_chunk @since(version: "25.1.1")
|
||||
cb_ai_chat_message_error @since(version: "25.1.1")
|
||||
cb_ai_chat_message @since(version: "25.3.5")
|
||||
|
||||
"Transaction count updated"
|
||||
cb_transaction_count @since(version: "24.3.3"),
|
||||
@@ -290,6 +291,18 @@ type WSAIChatMessageErrorEvent implements CBServerEvent @since(version: "25.1.1"
|
||||
errorMessage: String
|
||||
}
|
||||
|
||||
|
||||
type WSAIChatMessageEvent implements CBServerEvent @since(version: "25.3.5") {
|
||||
id: CBServerEventId!
|
||||
topicId: CBEventTopic
|
||||
conversationId: ID!
|
||||
messageId: ID!
|
||||
role: String!
|
||||
content: String!
|
||||
displayMessage: String!
|
||||
time: String!
|
||||
}
|
||||
|
||||
enum WSServerNotificationEventType {
|
||||
INFO
|
||||
ERROR
|
||||
|
||||
@@ -51,7 +51,9 @@ type SQLContextInfo {
|
||||
}
|
||||
|
||||
input SQLDataFilterConstraint {
|
||||
attributePosition: Int!
|
||||
"Either attributeName or attributePosition must be specified"
|
||||
attributeName: String
|
||||
attributePosition: Int
|
||||
orderPosition: Int
|
||||
orderAsc: Boolean
|
||||
|
||||
|
||||
+2
-1
@@ -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.
|
||||
@@ -82,6 +82,7 @@ public class WebDatabaseAuthModel {
|
||||
|
||||
return Arrays.stream(credentialsSource.getProperties())
|
||||
.filter(predicate)
|
||||
.filter(p -> !p.isDesktop())
|
||||
.map(p -> new WebPropertyInfo(webSession, p, credentialsSource)).toArray(WebPropertyInfo[]::new);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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.
|
||||
@@ -61,6 +61,7 @@ public class WebNetworkHandlerDescriptor {
|
||||
|
||||
public WebPropertyInfo[] getProperties() {
|
||||
return Arrays.stream(descriptor.getHandlerProperties())
|
||||
.filter(p -> !p.isDesktop() && !p.isHidden())
|
||||
.map(p -> new WebPropertyInfo(session, p, null)).toArray(WebPropertyInfo[]::new);
|
||||
}
|
||||
|
||||
|
||||
+2
-25
@@ -23,20 +23,14 @@ import org.jkiss.dbeaver.model.DBConstants;
|
||||
import org.jkiss.dbeaver.model.app.DBACertificateStorage;
|
||||
import org.jkiss.dbeaver.model.app.DBPWorkspace;
|
||||
import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage;
|
||||
import org.jkiss.dbeaver.model.qm.QMRegistry;
|
||||
import org.jkiss.dbeaver.model.qm.QMUtils;
|
||||
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
|
||||
import org.jkiss.dbeaver.runtime.SecurityProviderUtils;
|
||||
import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter;
|
||||
import org.jkiss.dbeaver.runtime.qm.QMRegistryImpl;
|
||||
|
||||
public abstract class BaseWebPlatform extends BaseServletPlatform {
|
||||
public static final String TEMP_FILE_FOLDER = "temp-sql-upload-files";
|
||||
public static final String TEMP_FILE_IMPORT_FOLDER = "temp-import-files";
|
||||
|
||||
|
||||
private QMRegistryImpl queryManager;
|
||||
private QMLogFileWriter qmLogWriter;
|
||||
private DBACertificateStorage certificateStorage;
|
||||
private ServerGlobalWorkspace workspace;
|
||||
|
||||
@@ -49,12 +43,8 @@ public abstract class BaseWebPlatform extends BaseServletPlatform {
|
||||
getApplication().beforeWorkspaceInitialization();
|
||||
this.workspace = new ServerGlobalWorkspace(this, getApplication());
|
||||
this.workspace.initializeProjects();
|
||||
QMUtils.initApplication(this);
|
||||
|
||||
this.queryManager = new QMRegistryImpl();
|
||||
|
||||
this.qmLogWriter = new QMLogFileWriter();
|
||||
this.queryManager.registerMetaListener(qmLogWriter);
|
||||
QMUtils.initPlatform(true);
|
||||
|
||||
this.certificateStorage = new DefaultCertificateStorage(
|
||||
this,
|
||||
@@ -93,21 +83,8 @@ public abstract class BaseWebPlatform extends BaseServletPlatform {
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
super.dispose();
|
||||
if (this.qmLogWriter != null) {
|
||||
this.queryManager.unregisterMetaListener(qmLogWriter);
|
||||
this.qmLogWriter.dispose();
|
||||
this.qmLogWriter = null;
|
||||
}
|
||||
if (this.queryManager != null) {
|
||||
this.queryManager.dispose();
|
||||
//queryManager = null;
|
||||
}
|
||||
QMUtils.disposePlatform();
|
||||
DataSourceProviderRegistry.dispose();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public QMRegistry getQueryManager() {
|
||||
return queryManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,13 +16,21 @@
|
||||
*/
|
||||
package io.cloudbeaver.server;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
|
||||
public class WebAppUtils {
|
||||
|
||||
public static boolean isWebApplication() {
|
||||
return DBWorkbench.getPlatform().getApplication() instanceof WebApplication;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebApplication getWebApplication() {
|
||||
return (WebApplication) DBWorkbench.getPlatform().getApplication();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BaseWebPlatform getWebPlatform() {
|
||||
return (BaseWebPlatform) DBWorkbench.getPlatform();
|
||||
}
|
||||
|
||||
+3
@@ -31,6 +31,9 @@ public class WSDefaultEventHandler<EVENT extends WSEvent> implements WSEventHand
|
||||
|
||||
@Override
|
||||
public void handleEvent(@NotNull EVENT event) {
|
||||
if (!WebAppUtils.isWebApplication()) {
|
||||
return;
|
||||
}
|
||||
log.debug(event.getTopicId() + " event handled");
|
||||
Collection<BaseWebSession> allSessions = WebAppUtils.getWebApplication()
|
||||
.getSessionManager()
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public class GraphQLLoggerUtil {
|
||||
public static String getUserId(@NotNull HttpServletRequest request) {
|
||||
WebSession session = getWebSession(request);
|
||||
if (session == null) {
|
||||
return null;
|
||||
return QMConstants.QM_UNAUTHORIZED_USER;
|
||||
}
|
||||
String userId = session.getUserContext().getUserId();
|
||||
if (userId == null) {
|
||||
|
||||
+2
-2
@@ -17,7 +17,6 @@
|
||||
package io.cloudbeaver.server.servlets;
|
||||
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import io.cloudbeaver.server.WebAppUtils;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
@@ -27,6 +26,7 @@ import org.eclipse.jetty.ee11.servlet.DefaultServlet;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -39,7 +39,7 @@ public class WebStatusServlet extends DefaultServlet {
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
response.setContentType(CBConstants.APPLICATION_JSON);
|
||||
response.setContentType(HttpConstants.CONTENT_TYPE_JSON);
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
Map<String, Object> infoMap = new LinkedHashMap<>();
|
||||
infoMap.put("health", "ok");
|
||||
|
||||
+3
-4
@@ -93,7 +93,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
return;
|
||||
}
|
||||
|
||||
resp.setContentType(CBConstants.APPLICATION_JSON);
|
||||
resp.setContentType(HttpConstants.CONTENT_TYPE_JSON);
|
||||
WSUtils.clientGson.toJson(Map.of("events", events), resp.getWriter());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
@@ -187,8 +187,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
|
||||
private void addCorsHeaders(@NotNull HttpServletRequest req, @NotNull HttpServletResponse resp) {
|
||||
String origin = req.getHeader("Origin");
|
||||
boolean develMode = ServletAppUtils.getServletApplication().getServerConfiguration().isDevelMode();
|
||||
if (!develMode || origin == null) {
|
||||
if (origin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,7 +238,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
) {
|
||||
try {
|
||||
resp.setStatus(status);
|
||||
resp.setContentType(CBConstants.APPLICATION_JSON);
|
||||
resp.setContentType(HttpConstants.CONTENT_TYPE_JSON);
|
||||
|
||||
Map<String, Object> error = Map.of(
|
||||
"error", message,
|
||||
|
||||
+6
-1
@@ -160,7 +160,12 @@ public interface DBWServiceCore extends DBWService {
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
WebNetworkEndpointInfo testNetworkHandler(@NotNull WebSession webSession, @NotNull WebNetworkHandlerConfigInput nhConfig) throws DBWebException;
|
||||
WebNetworkEndpointInfo testNetworkHandler(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String projectId,
|
||||
@Nullable String connectionId,
|
||||
@NotNull WebNetworkHandlerConfigInput nhConfig
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
WebConnectionInfo closeConnection(@NotNull WebSession webSession, @Nullable String projectId, @NotNull String connectionId) throws DBWebException;
|
||||
|
||||
+4
-1
@@ -139,7 +139,10 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
|
||||
getWebSession(env), getProjectReference(env), getArgumentVal(env, "config")
|
||||
))
|
||||
.dataFetcher("testNetworkHandler", env -> getService(env).testNetworkHandler(
|
||||
getWebSession(env), new WebNetworkHandlerConfigInput(getArgument(env, "config"))
|
||||
getWebSession(env),
|
||||
getProjectReference(env),
|
||||
getArgument(env, "connectionId"),
|
||||
new WebNetworkHandlerConfigInput(getArgument(env, "config"))
|
||||
))
|
||||
.dataFetcher("closeConnection", env -> getService(env).closeConnection(
|
||||
getWebSession(env),
|
||||
|
||||
+25
-1
@@ -591,6 +591,8 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
@Override
|
||||
public WebNetworkEndpointInfo testNetworkHandler(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String projectId,
|
||||
@Nullable String connectionId,
|
||||
@NotNull WebNetworkHandlerConfigInput nhConfig
|
||||
) throws DBWebException {
|
||||
DBRProgressMonitor monitor = webSession.getProgressMonitor();
|
||||
@@ -611,7 +613,13 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
try {
|
||||
monitor.subTask("Initialize tunnel");
|
||||
|
||||
DBWHandlerConfiguration configuration = new DBWHandlerConfiguration(handlerDescriptor, null);
|
||||
DBWHandlerConfiguration currentConnectionHandlerConfig =
|
||||
getCurrentConnectionHandlerConfig(webSession, projectId, connectionId, nhConfig.getId());
|
||||
|
||||
DBWHandlerConfiguration configuration = currentConnectionHandlerConfig == null
|
||||
? new DBWHandlerConfiguration(handlerDescriptor, null)
|
||||
: new DBWHandlerConfiguration(currentConnectionHandlerConfig);
|
||||
|
||||
WebDataSourceUtils.updateHandlerConfig(configuration, nhConfig);
|
||||
configuration.setSavePassword(true);
|
||||
configuration.setEnabled(true);
|
||||
@@ -642,6 +650,22 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DBWHandlerConfiguration getCurrentConnectionHandlerConfig(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String projectId,
|
||||
@Nullable String connectionId,
|
||||
@NotNull String handlerConfigId
|
||||
) throws DBWebException {
|
||||
if (projectId == null || connectionId == null) {
|
||||
return null;
|
||||
}
|
||||
WebSessionProjectImpl sessionProject = webSession.getAccessibleProjectById(projectId);
|
||||
WebConnectionInfo connectionInfo = sessionProject.getWebConnectionInfo(connectionId);
|
||||
DBPConnectionConfiguration connectionConfiguration = connectionInfo.getDataSourceContainer().getConnectionConfiguration();
|
||||
return connectionConfiguration.getHandler(handlerConfigId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebConnectionInfo closeConnection(
|
||||
@NotNull WebSession webSession,
|
||||
|
||||
+9
-9
@@ -145,11 +145,6 @@ public class WebDefaultFeatureProvider implements DBWFeatureProvider {
|
||||
return features;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getProjectId(@NotNull DBNNode node) {
|
||||
return node.getOwnerProject().getId();
|
||||
}
|
||||
|
||||
private boolean canCreateConnectionFromFileName(String fileName) {
|
||||
String fileExtension = IOUtils.getFileExtension(fileName);
|
||||
if (CommonUtils.isEmpty(fileExtension)) {
|
||||
@@ -169,11 +164,16 @@ public class WebDefaultFeatureProvider implements DBWFeatureProvider {
|
||||
}
|
||||
|
||||
private boolean hasNodePermission(@NotNull WebSession webSession, @NotNull DBNNode node, @NotNull RMProjectPermission permission) {
|
||||
WebProjectImpl project = webSession.getProjectById(node.getOwnerProject().getId());
|
||||
if (project == null) {
|
||||
return false;
|
||||
RMProject rmProject;
|
||||
if (node instanceof DBNResourceManagerResource rmr) {
|
||||
rmProject = rmr.getRmProject();
|
||||
} else {
|
||||
WebProjectImpl project = webSession.getProjectById(node.getOwnerProject().getId());
|
||||
if (project == null) {
|
||||
return false;
|
||||
}
|
||||
rmProject = project.getRMProject();
|
||||
}
|
||||
RMProject rmProject = project.getRMProject();
|
||||
return SMUtils.hasProjectPermission(webSession, rmProject, permission);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class WebSQLCompletionContext implements SQLCompletionContext {
|
||||
|
||||
@Override
|
||||
public boolean isSearchInsideNames() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+44
-7
@@ -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.
|
||||
@@ -39,7 +39,7 @@ public class WebSQLDataFilter {
|
||||
|
||||
|
||||
private int offset;
|
||||
private int limit ;
|
||||
private int limit;
|
||||
private String where;
|
||||
private final List<WebSQLDataFilterConstraint> constraints = new ArrayList<>();
|
||||
|
||||
@@ -48,7 +48,7 @@ public class WebSQLDataFilter {
|
||||
this.limit = DEFAULT_ROWS_NUMBER;
|
||||
}
|
||||
|
||||
public WebSQLDataFilter(Map<String, Object> filterProps) {
|
||||
public WebSQLDataFilter(@NotNull Map<String, Object> filterProps) {
|
||||
this.offset = CommonUtils.toInt(filterProps.get("offset"));
|
||||
this.limit = CommonUtils.toInt(filterProps.get("limit"));
|
||||
if (this.limit <= 0) {
|
||||
@@ -59,10 +59,10 @@ public class WebSQLDataFilter {
|
||||
this.where = CommonUtils.toString(filterProps.get("where"), null);
|
||||
Object constraints = filterProps.get("constraints");
|
||||
if (constraints instanceof Collection) {
|
||||
for (Object constrItem : (Collection<?>)constraints) {
|
||||
for (Object constrItem : (Collection<?>) constraints) {
|
||||
if (constrItem instanceof Map) {
|
||||
this.constraints.add(
|
||||
new WebSQLDataFilterConstraint((Map<String, Object>)constrItem));
|
||||
new WebSQLDataFilterConstraint((Map<String, Object>) constrItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,8 @@ public class WebSQLDataFilter {
|
||||
return where;
|
||||
}
|
||||
|
||||
public static WebSQLDataFilter from(DBDDataFilter filter) {
|
||||
@NotNull
|
||||
public static WebSQLDataFilter from(@NotNull DBDDataFilter filter) {
|
||||
var webFilter = new WebSQLDataFilter();
|
||||
webFilter.where = filter.getWhere();
|
||||
for (DBDAttributeConstraint constraint : filter.getConstraints()) {
|
||||
@@ -101,6 +102,7 @@ public class WebSQLDataFilter {
|
||||
return webFilter;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DBDDataFilter makeDataFilter(@Nullable WebSQLResultsInfo resultInfo) throws DBException {
|
||||
DBDDataFilter dataFilter = new DBDDataFilter();
|
||||
dataFilter.setWhere(where);
|
||||
@@ -112,12 +114,17 @@ public class WebSQLDataFilter {
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
private List<DBDAttributeConstraint> mapWebConstrainsToDbdConstrains(@Nullable WebSQLResultsInfo resultInfo) throws DBException {
|
||||
if (resultInfo == null) {
|
||||
return getDbdConstraints();
|
||||
}
|
||||
List<DBDAttributeConstraint> constraints = generateEmptyConstrains(resultInfo);
|
||||
fillEmptyConstrains(constraints);
|
||||
return constraints;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<DBDAttributeConstraint> generateEmptyConstrains(@Nullable WebSQLResultsInfo resultInfo) {
|
||||
if (resultInfo == null) {
|
||||
return Collections.emptyList();
|
||||
@@ -132,7 +139,7 @@ public class WebSQLDataFilter {
|
||||
|
||||
private void fillEmptyConstrains(@NotNull List<DBDAttributeConstraint> emptyConstraints) throws DBException {
|
||||
for (WebSQLDataFilterConstraint webConstr : constraints) {
|
||||
if(webConstr.getAttributePosition() >= emptyConstraints.size()) {
|
||||
if (webConstr.getAttributePosition() >= emptyConstraints.size()) {
|
||||
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", webConstr.getAttributePosition()));
|
||||
}
|
||||
DBDAttributeConstraint dbConstr = emptyConstraints.get(webConstr.getAttributePosition());
|
||||
@@ -140,6 +147,7 @@ public class WebSQLDataFilter {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBDAttributeConstraint fillEmptyConstraint(@NotNull DBDAttributeConstraint dbConstr,
|
||||
@NotNull WebSQLDataFilterConstraint webConstr) {
|
||||
dbConstr.setPlainNameReference(true);
|
||||
@@ -159,4 +167,33 @@ public class WebSQLDataFilter {
|
||||
}
|
||||
return dbConstr;
|
||||
}
|
||||
|
||||
private List<DBDAttributeConstraint> getDbdConstraints() {
|
||||
List<DBDAttributeConstraint> dbConstraints = new ArrayList<>();
|
||||
for (WebSQLDataFilterConstraint constraint : constraints) {
|
||||
String attributeName = constraint.getAttributeName();
|
||||
DBDAttributeConstraint dbConstraint = new DBDAttributeConstraint(
|
||||
attributeName, getOriginalVisualPosition(constraint, attributeName)
|
||||
);
|
||||
|
||||
fillEmptyConstraint(dbConstraint, constraint);
|
||||
dbConstraints.add(dbConstraint);
|
||||
}
|
||||
return dbConstraints;
|
||||
}
|
||||
|
||||
private static int getOriginalVisualPosition(WebSQLDataFilterConstraint constraint, String attributeName) {
|
||||
Integer attributePosition = constraint.getAttributePosition();
|
||||
|
||||
if (CommonUtils.isEmpty(attributeName)) {
|
||||
if (attributePosition == null) {
|
||||
throw new IllegalArgumentException("Constraint must specify either attributePosition or attributeName");
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Can't convert constraint by attributePosition without result set metadata (attributeName is required)"
|
||||
);
|
||||
}
|
||||
|
||||
return attributePosition == null ? -1 : attributePosition;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -184,8 +184,9 @@ public class WebSQLQueryResultSet {
|
||||
/**
|
||||
* Sets info about read-only status of a result set.
|
||||
*/
|
||||
public void setReadOnlyInfo(@NotNull DBCExecutionContext executionContext) {
|
||||
public void setReadOnlyInfo(@Nullable DBCExecutionContext executionContext) {
|
||||
this.readOnly = DBExecUtils.isResultSetReadOnly(executionContext);
|
||||
this.readOnlyStatus = DBExecUtils.getResultSetReadOnlyStatus(executionContext.getDataSource().getContainer());
|
||||
this.readOnlyStatus = DBExecUtils.getResultSetReadOnlyStatus(
|
||||
executionContext == null ? null : executionContext.getDataSource().getContainer());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -64,7 +65,7 @@ public class WebSQLResultServlet extends WebServiceServletBase {
|
||||
}
|
||||
Path dataFile = getDataFile(request);
|
||||
session.addInfoMessage("Download LOB file ...");
|
||||
response.setHeader("Content-Type", "application/octet-stream");
|
||||
response.setHeader(HttpConstants.HEADER_CONTENT_TYPE, "application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + dataFile.getFileName().toString() + "\"");
|
||||
response.setHeader("Content-Length", String.valueOf(Files.size(dataFile)));
|
||||
response.setDateHeader("Expires", System.currentTimeMillis() + CBConstants.STATIC_CACHE_SECONDS * 1000);
|
||||
|
||||
+29
@@ -31,6 +31,7 @@ import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBPDataKind;
|
||||
import org.jkiss.dbeaver.model.DBPDataSource;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
@@ -256,10 +257,38 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
@NotNull String resultsId,
|
||||
@NotNull List<WebSQLResultsRow> selectedRows
|
||||
) throws DBWebException {
|
||||
checkAndFillTruncatedData(sqlContext, resultsId, selectedRows);
|
||||
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows);
|
||||
return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider));
|
||||
}
|
||||
|
||||
private void checkAndFillTruncatedData(
|
||||
@NotNull WebSQLContextInfo sqlContext,
|
||||
@NotNull String resultsId,
|
||||
@NotNull List<WebSQLResultsRow> selectedRows
|
||||
) throws DBWebException {
|
||||
List<DBDAttributeBinding> attributes = Arrays.stream(sqlContext.getResults(resultsId).getAttributes())
|
||||
.filter(attr -> canBeTruncated(attr.getDataKind()))
|
||||
.toList();
|
||||
for (WebSQLResultsRow row : selectedRows) {
|
||||
Object[] data = row.getData();
|
||||
for (DBDAttributeBinding attribute : attributes) {
|
||||
int position = attribute.getOrdinalPosition();
|
||||
boolean valueIsTruncated = data[position] != null &&
|
||||
data[position].toString().length() == WebSQLConstants.TEXT_PREVIEW_MAX_LENGTH;
|
||||
if (valueIsTruncated) {
|
||||
data[position] = getCellValue(sqlContext, resultsId, position, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canBeTruncated(@NotNull DBPDataKind dataKind) {
|
||||
return dataKind.equals(DBPDataKind.STRING) ||
|
||||
dataKind.equals(DBPDataKind.CONTENT) ||
|
||||
dataKind.equals(DBPDataKind.BINARY);
|
||||
}
|
||||
|
||||
private String createAndRunGenerator(
|
||||
@NotNull WebSession session,
|
||||
@NotNull String generatorId,
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - Administration
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
|
||||
Bundle-Version: 1.0.140.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.141.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.admin</artifactId>
|
||||
<version>1.0.140-SNAPSHOT</version>
|
||||
<version>1.0.141-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+4
-3
@@ -5,15 +5,16 @@ import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.service.WebServiceServletBase;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBConstants;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.dbeaver.utils.MimeTypes;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -47,7 +48,7 @@ public class WebAdminLogsServlet extends WebServiceServletBase {
|
||||
throw new DBWebException("Log file '" + logFileName + "' not found");
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", MimeTypes.TEXT_PLAIN);
|
||||
response.setHeader(HttpConstants.HEADER_CONTENT_TYPE, MimeTypes.TEXT_PLAIN);
|
||||
if (logFileName.equals(".log")) {
|
||||
logFileName = "server.log";
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - Authentication
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true
|
||||
Bundle-Version: 1.0.140.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.141.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.auth</artifactId>
|
||||
<version>1.0.140-SNAPSHOT</version>
|
||||
<version>1.0.141-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - Data Transfer
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true
|
||||
Bundle-Version: 1.0.141.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.142.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.data.transfer</artifactId>
|
||||
<version>1.0.141-SNAPSHOT</version>
|
||||
<version>1.0.142-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+2
-2
@@ -23,7 +23,6 @@ import io.cloudbeaver.model.WebAsyncTaskInfo;
|
||||
import io.cloudbeaver.model.WebConnectionInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.BaseWebPlatform;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import io.cloudbeaver.server.WebAppUtils;
|
||||
import io.cloudbeaver.server.WebApplication;
|
||||
import io.cloudbeaver.service.WebServiceServletBase;
|
||||
@@ -40,6 +39,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -121,7 +121,7 @@ public class WebDataTransferImportServlet extends WebServiceServletBase {
|
||||
|
||||
WebAsyncTaskInfo asyncImportDataContainer =
|
||||
dbwServiceDataTransfer.asyncImportDataContainer(processorId, filePath, webSQLResultsInfo, session);
|
||||
response.setContentType(CBConstants.APPLICATION_JSON);
|
||||
response.setContentType(HttpConstants.CONTENT_TYPE_JSON);
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
parameters.put("id", asyncImportDataContainer.getId());
|
||||
parameters.put("name", asyncImportDataContainer.getName());
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@ import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor;
|
||||
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferRegistry;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -69,7 +70,7 @@ public class WebDataTransferServlet extends WebServiceServletBase {
|
||||
}
|
||||
fileName = WebDataTransferUtils.normalizeFileName(fileName, taskInfo.getParameters().getOutputSettings());
|
||||
session.addInfoMessage("Download data ...");
|
||||
response.setHeader("Content-Type", processor.getContentType());
|
||||
response.setHeader(HttpConstants.HEADER_CONTENT_TYPE, processor.getContentType());
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
|
||||
|
||||
dtManager.exportDataTransferToStream(session.getProgressMonitor(), taskInfo, response.getOutputStream());
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - File System
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.fs;singleton:=true
|
||||
Bundle-Version: 1.0.58.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.59.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.fs</artifactId>
|
||||
<version>1.0.58-SNAPSHOT</version>
|
||||
<version>1.0.59-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+2
-1
@@ -35,6 +35,7 @@ import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
import org.jkiss.utils.HttpConstants;
|
||||
import org.jkiss.utils.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -76,7 +77,7 @@ public class WebFSServlet extends WebServiceServletBase {
|
||||
private void doGet(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
|
||||
Path path = WebFSUtils.getPathFromNode(session, request.getParameter("nodePath"));
|
||||
session.addInfoMessage("Download data ...");
|
||||
response.setHeader("Content-Type", "application/octet-stream");
|
||||
response.setHeader(HttpConstants.HEADER_CONTENT_TYPE, "application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + path.getFileName() + "\"");
|
||||
response.setHeader("Content-Length", String.valueOf(Files.size(path)));
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Cloudbeaver Web Service - LDAP
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.ldap.auth;singleton:=true
|
||||
Bundle-Version: 1.0.23.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.24.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.ldap.auth</artifactId>
|
||||
<version>1.0.23-SNAPSHOT</version>
|
||||
<version>1.0.24-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - Metadata
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true
|
||||
Bundle-Version: 1.0.144.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.145.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.metadata</artifactId>
|
||||
<version>1.0.144-SNAPSHOT</version>
|
||||
<version>1.0.145-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Resource manager NIO implementation
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.rm.nio;singleton:=true
|
||||
Bundle-Version: 1.0.58.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.59.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.model,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.rm.nio</artifactId>
|
||||
<version>1.0.58-SNAPSHOT</version>
|
||||
<version>1.0.59-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - Resource manager
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.rm;singleton:=true
|
||||
Bundle-Version: 1.0.93.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.94.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</extension>
|
||||
|
||||
<extension point="org.jkiss.dbeaver.navigator">
|
||||
<extender id="rm" root="true" class="io.cloudbeaver.service.rm.RMNavigatorModelExtender"/>
|
||||
<extender id="rm" root="true" nodeDisplayName="Resources" nodeType="io.cloudbeaver.model.rm.DBNResourceManagerRoot" class="io.cloudbeaver.service.rm.RMNavigatorModelExtender"/>
|
||||
</extension>
|
||||
|
||||
</plugin>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.rm</artifactId>
|
||||
<version>1.0.93-SNAPSHOT</version>
|
||||
<version>1.0.94-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+3
-9
@@ -27,20 +27,14 @@ import org.jkiss.dbeaver.model.navigator.DBNRoot;
|
||||
public class RMNavigatorModelExtender implements DBNModelExtender {
|
||||
|
||||
@Override
|
||||
public DBNNode[] getExtraNodes(@NotNull DBNNode parentNode) {
|
||||
if (parentNode instanceof DBNRoot && ServletAppUtils.getServletApplication()
|
||||
public DBNNode createNode(@NotNull DBNNode parentNode) {
|
||||
if (parentNode instanceof DBNRoot root && ServletAppUtils.getServletApplication()
|
||||
.getAppConfiguration()
|
||||
.isResourceManagerEnabled()) {
|
||||
return createRMNodes((DBNRoot) parentNode);
|
||||
return new DBNResourceManagerRoot(root);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private DBNNode[] createRMNodes(DBNRoot root) {
|
||||
return new DBNNode[]{
|
||||
new DBNResourceManagerRoot(root)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Cloudbeaver Web Service - Security
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true
|
||||
Bundle-Version: 1.0.96.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.97.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.security</artifactId>
|
||||
<version>1.0.96-SNAPSHOT</version>
|
||||
<version>1.0.97-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: CloudBeaver SLF4j Binding
|
||||
Bundle-SymbolicName: io.cloudbeaver.slf4j;singleton:=true
|
||||
Bundle-Version: 1.0.56.qualifier
|
||||
Bundle-Release-Date: 20260302
|
||||
Bundle-Version: 1.0.57.qualifier
|
||||
Bundle-Release-Date: 20260323
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Fragment-Host: slf4j.api
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.slf4j</artifactId>
|
||||
<version>1.0.56-SNAPSHOT</version>
|
||||
<version>1.0.57-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<dependency>
|
||||
<groupId>com.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
<version>0.9.5</version>
|
||||
<version>0.9.7</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.ce.drivers.feature"
|
||||
label="Cloudbeaver CE 3rd Party Drivers"
|
||||
version="1.0.164.qualifier"
|
||||
version="1.0.165.qualifier"
|
||||
provider-name="DBeaver Corp">
|
||||
|
||||
<plugin id="io.cloudbeaver.resources.drivers.base" version="0.0.0"/>
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.ce.drivers.feature</artifactId>
|
||||
<version>1.0.164-SNAPSHOT</version>
|
||||
<version>1.0.165-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
</project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.product.ce.feature"
|
||||
label="%featureName"
|
||||
version="26.0.0.qualifier"
|
||||
version="26.0.1.qualifier"
|
||||
provider-name="%providerName"
|
||||
plugin="io.cloudbeaver.product.ce">
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.product.ce.feature</artifactId>
|
||||
<version>26.0.0-SNAPSHOT</version>
|
||||
<version>26.0.1-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.server.feature"
|
||||
label="%featureName"
|
||||
version="26.0.0.qualifier"
|
||||
version="26.0.1.qualifier"
|
||||
provider-name="%providerName"
|
||||
plugin="io.cloudbeaver.server">
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server.feature</artifactId>
|
||||
<version>26.0.0-SNAPSHOT</version>
|
||||
<version>26.0.1-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
</project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<feature
|
||||
id="io.cloudbeaver.ws.feature"
|
||||
label="%featureName"
|
||||
version="1.0.94.qualifier"
|
||||
version="1.0.95.qualifier"
|
||||
provider-name="%providerName"
|
||||
plugin="io.cloudbeaver.model">
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.ws.feature</artifactId>
|
||||
<version>1.0.94-SNAPSHOT</version>
|
||||
<version>1.0.95-SNAPSHOT</version>
|
||||
<packaging>eclipse-feature</packaging>
|
||||
</project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?pde version="3.5"?>
|
||||
|
||||
<product name="CloudBeaver Community" uid="io.cloudbeaver.product" id="io.cloudbeaver.product.ce.product"
|
||||
application="io.cloudbeaver.product.ce.application" version="26.0.0.qualifier" useFeatures="true"
|
||||
application="io.cloudbeaver.product.ce.application" version="26.0.1.qualifier" useFeatures="true"
|
||||
includeLaunchers="false">
|
||||
|
||||
<configIni use="default">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../</relativePath>
|
||||
</parent>
|
||||
<version>26.0.0-SNAPSHOT</version>
|
||||
<version>26.0.1-SNAPSHOT</version>
|
||||
<artifactId>web-server</artifactId>
|
||||
<packaging>eclipse-repository</packaging>
|
||||
<name>Cloudbeaver Server Product</name>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"./*": "./lib/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"clear": "rimraf lib"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -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.
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { memo, use, useMemo } from 'react';
|
||||
import { Cell, type CellRendererProps } from 'react-data-grid';
|
||||
import { clsx } from '@dbeaver/ui-kit';
|
||||
import { DataGridCellContext, type IDataGridCellRenderer } from './DataGridCellContext.js';
|
||||
import { createCellMouseEvent } from './eventUtils.js';
|
||||
import { DataGridCellInnerContext, type IDataGridCellInnerContext } from './DataGridCellInnerContext.js';
|
||||
@@ -21,6 +22,7 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
|
||||
const dataColIdx = dndContext.getDataColIdxByKey(props.column.key);
|
||||
const rowIdx = props.rowIdx;
|
||||
const tooltip = useGridReactiveValue(cellContext?.cellTooltip, rowIdx, dataColIdx);
|
||||
const cellClassName = useGridReactiveValue(cellContext?.getCellClassName, rowIdx, dataColIdx);
|
||||
|
||||
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onCellClick?.(
|
||||
@@ -85,10 +87,11 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
|
||||
|
||||
const renderDefaultCell = useMemo<IDataGridCellRenderer>(
|
||||
() =>
|
||||
({ onClick, onDoubleClick, onContextMenu, ...rest }) => (
|
||||
({ onClick, onDoubleClick, onContextMenu, className, ...rest }) => (
|
||||
<Cell
|
||||
{...props}
|
||||
title={tooltip}
|
||||
className={clsx(cellClassName, className)}
|
||||
onCellClick={(_, event) => onClick?.(event)}
|
||||
onCellDoubleClick={(_, event) => onDoubleClick?.(event)}
|
||||
onCellContextMenu={(_, event) => onContextMenu?.(event)}
|
||||
@@ -96,11 +99,15 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
|
||||
isCellSelected={props.isCellSelected || rest.isFocused || false}
|
||||
/>
|
||||
),
|
||||
[...Object.values(props), tooltip],
|
||||
[...Object.values(props), tooltip, cellClassName],
|
||||
);
|
||||
|
||||
const cellElement = useGridReactiveValue(cellContext?.cellElement, rowIdx, dataColIdx, mappedProps, renderDefaultCell);
|
||||
|
||||
const innerCellContext = useMemo<IDataGridCellInnerContext>(() => ({ isFocused: props.isCellSelected }), [props.isCellSelected]);
|
||||
return <DataGridCellInnerContext value={innerCellContext}>{cellElement ?? <Cell title={tooltip} {...props} />}</DataGridCellInnerContext>;
|
||||
return (
|
||||
<DataGridCellInnerContext value={innerCellContext}>
|
||||
{cellElement ?? <Cell title={tooltip} className={cellClassName} {...props} />}
|
||||
</DataGridCellInnerContext>
|
||||
);
|
||||
}) as <TRow, TSummaryRow>(props: CellRendererProps<TRow, TSummaryRow>) => React.ReactNode;
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
@import url('@dbeaver/ui-kit/index.css');
|
||||
@import url('react-data-grid/lib/styles.css') layer(components);
|
||||
|
||||
@layer components.rdg.Search {
|
||||
.rdg-search-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rdg-search-panel {
|
||||
padding-bottom: 0 !important;
|
||||
.search-panel__inputs {
|
||||
max-width: var(--tw-container-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.rdg-cell-search-match {
|
||||
background-color: #ffff0054;
|
||||
}
|
||||
|
||||
.rdg-cell-search-active {
|
||||
background-color: #ff6a0054;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components.rdg.Cell {
|
||||
.rdg {
|
||||
--dbv-kit-icon-btn-outline-width: 1px;
|
||||
|
||||
@@ -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 { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { Activity, forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
DataGrid as DataGridBase,
|
||||
type ColumnOrColumnGroup,
|
||||
@@ -29,6 +29,9 @@ import { mapEditCellRenderer } from './mapEditCellRenderer.js';
|
||||
import { DataGridRowContext, type IDataGridRowContext } from './DataGridRowContext.js';
|
||||
import './DataGrid.css';
|
||||
import { HeaderDnDContext, isColumn, useHeaderDnD } from './useHeaderDnD.js';
|
||||
import type { IGridSearchStorage } from './search/useGridSearch.js';
|
||||
import { GridSearchPanel } from './search/GridSearchPanel.js';
|
||||
import { useDataGridSearch } from './useDataGridSearch.js';
|
||||
|
||||
export interface ICellPosition {
|
||||
rowIdx: number;
|
||||
@@ -51,13 +54,22 @@ export interface DataGridProps extends IDataGridCellContext, IDataGridRowContext
|
||||
onEditorOpen?: (position: ICellPosition) => void;
|
||||
onCellKeyDown?: (position: ICellPosition, event: DataGridCellKeyboardEvent) => void;
|
||||
className?: string;
|
||||
search?: {
|
||||
isEnabled?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
storage?: IGridSearchStorage;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DataGridRef {
|
||||
selectCell: (position: ICellPosition) => void;
|
||||
selectCell: (position: ICellPosition, options?: { deferred?: boolean }) => boolean;
|
||||
scrollToCell: (position: Partial<ICellPosition>) => void;
|
||||
openEditor: (position: ICellPosition) => void;
|
||||
restoreFocus: () => void;
|
||||
getColumnsOrdered: () => readonly CalculatedColumn<IInnerRow, unknown>[];
|
||||
openSearch: () => void;
|
||||
closeSearch: () => void;
|
||||
refreshSearch: () => void;
|
||||
}
|
||||
|
||||
const MAX_AUTO_SIZE_WIDTH = 350;
|
||||
@@ -92,11 +104,13 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
onScrollToBottom,
|
||||
onFocus,
|
||||
onCellChange,
|
||||
onCellChangeBatch,
|
||||
onColumnSort,
|
||||
onHeaderKeyDown,
|
||||
children,
|
||||
className,
|
||||
onCellKeyDown,
|
||||
search: { isEnabled: searchEnabled, isReadOnly: searchReadOnly, storage: searchStorage } = {},
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -104,8 +118,26 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
const rowsCount = useGridReactiveValue(rowCount);
|
||||
const columnsCount = useGridReactiveValue(columnCount);
|
||||
|
||||
const rowsCountRef = useRef(rowsCount);
|
||||
const [prevRowsCount, setPrevRowsCount] = useState(rowsCount);
|
||||
const innerGridRef = useRef<DataGridHandle<IInnerRow, unknown>>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const deferredSelectRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const {
|
||||
searchOpen,
|
||||
searchCellClassName,
|
||||
searchPanelRef,
|
||||
setSearchPanelRef,
|
||||
isReplacingRef,
|
||||
handleSearchOpen,
|
||||
handleSearchClose,
|
||||
onReplace,
|
||||
handleReplacingChange,
|
||||
} = useDataGridSearch({ containerRef, searchStorage, getCellEditable, onCellChangeBatch });
|
||||
|
||||
function scrollToCell(rowIdx: number, colIdx: number) {
|
||||
innerGridRef.current?.scrollToCell({ idx: colIdx, rowIdx });
|
||||
}
|
||||
|
||||
const columns = new Array<ColumnOrColumnGroup<IInnerRow, unknown>>(columnsCount)
|
||||
.fill(null as any)
|
||||
.map((_, i): ColumnOrColumnGroup<IInnerRow, unknown> => {
|
||||
@@ -141,15 +173,37 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
return null;
|
||||
}
|
||||
|
||||
function restoreFocusInternal() {
|
||||
const focusSink = containerRef.current?.querySelector<HTMLDivElement>('[aria-selected="true"]');
|
||||
focusSink?.focus();
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
selectCell: (position: ICellPosition) => {
|
||||
selectCell: (position: ICellPosition, options?: { deferred?: boolean }) => {
|
||||
if (isReplacingRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const columnKey = mapPositionToColumnKey(position);
|
||||
|
||||
if (!columnKey) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
innerGridRef.current?.selectCellByKey({ columnKey, rowIdx: position.rowIdx });
|
||||
if (options?.deferred) {
|
||||
if (deferredSelectRef.current !== null) {
|
||||
clearTimeout(deferredSelectRef.current);
|
||||
}
|
||||
deferredSelectRef.current = setTimeout(() => {
|
||||
deferredSelectRef.current = null;
|
||||
innerGridRef.current?.selectCellByKey({ columnKey, rowIdx: position.rowIdx });
|
||||
requestAnimationFrame(restoreFocusInternal);
|
||||
}, 1);
|
||||
} else {
|
||||
innerGridRef.current?.selectCellByKey({ columnKey, rowIdx: position.rowIdx });
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
scrollToCell: (position: Partial<ICellPosition>) => {
|
||||
innerGridRef.current?.scrollToCell({ idx: position.colIdx && dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
|
||||
@@ -168,14 +222,17 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
},
|
||||
);
|
||||
},
|
||||
restoreFocus: restoreFocusInternal,
|
||||
getColumnsOrdered: () => innerGridRef.current?.getColumnsOrdered() ?? [],
|
||||
openSearch: handleSearchOpen,
|
||||
closeSearch: handleSearchClose,
|
||||
refreshSearch: () => searchPanelRef.current?.refresh(),
|
||||
}));
|
||||
|
||||
if (rowsCountRef.current !== rowsCount) {
|
||||
const previousRowCount = rowsCountRef.current;
|
||||
rowsCountRef.current = rowsCount;
|
||||
if (prevRowsCount !== rowsCount) {
|
||||
setPrevRowsCount(rowsCount);
|
||||
|
||||
if (previousRowCount === 0) {
|
||||
if (prevRowsCount === 0) {
|
||||
setColumnWidths(new Map<string, ColumnWidth>());
|
||||
}
|
||||
}
|
||||
@@ -189,6 +246,9 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
);
|
||||
|
||||
function handleCellFocus(args: CellSelectArgs<IInnerRow, unknown>) {
|
||||
if (isReplacingRef.current) {
|
||||
return;
|
||||
}
|
||||
onFocus?.({ colIdx: dndHeaderContext.getDataColIdxByKey(args.column.key), rowIdx: args.rowIdx });
|
||||
}
|
||||
|
||||
@@ -214,44 +274,59 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderDnDContext value={dndHeaderContext}>
|
||||
<DataGridRowContext value={{ rowElement, rowCount, onScrollToBottom }}>
|
||||
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
|
||||
<DataGridCellHeaderContext
|
||||
value={{
|
||||
headerElement,
|
||||
headerText,
|
||||
getHeaderDnD,
|
||||
columnSortable,
|
||||
onColumnSort,
|
||||
columnSortingState,
|
||||
onHeaderKeyDown,
|
||||
columnSortingMultiple,
|
||||
}}
|
||||
>
|
||||
<DataGridBase
|
||||
ref={innerGridRef}
|
||||
columns={dndHeaderContext.columns}
|
||||
rows={rows}
|
||||
className={className}
|
||||
headerRowHeight={getHeaderHeight?.()}
|
||||
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
|
||||
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
|
||||
rowClass={getRowClass ? row => getRowClass(row.idx) : undefined}
|
||||
columnWidths={columnWidths}
|
||||
renderers={{
|
||||
renderRow: rowRenderer,
|
||||
renderCell: cellRenderer,
|
||||
noRowsFallback: children,
|
||||
<div ref={containerRef} className="rdg-search-container">
|
||||
<HeaderDnDContext value={dndHeaderContext}>
|
||||
<DataGridRowContext value={{ rowElement, rowCount, onScrollToBottom }}>
|
||||
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, getCellClassName: searchCellClassName, onCellChange }}>
|
||||
<DataGridCellHeaderContext
|
||||
value={{
|
||||
headerElement,
|
||||
headerText,
|
||||
getHeaderDnD,
|
||||
columnSortable,
|
||||
onColumnSort,
|
||||
columnSortingState,
|
||||
onHeaderKeyDown,
|
||||
columnSortingMultiple,
|
||||
}}
|
||||
onScroll={onScroll}
|
||||
onSelectedCellChange={handleCellFocus}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
onColumnWidthsChange={setColumnWidths}
|
||||
/>
|
||||
</DataGridCellHeaderContext>
|
||||
</DataGridCellContext>
|
||||
</DataGridRowContext>
|
||||
</HeaderDnDContext>
|
||||
>
|
||||
<DataGridBase
|
||||
ref={innerGridRef}
|
||||
columns={dndHeaderContext.columns}
|
||||
rows={rows}
|
||||
className={className}
|
||||
headerRowHeight={getHeaderHeight?.()}
|
||||
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
|
||||
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
|
||||
rowClass={getRowClass ? row => getRowClass(row.idx) : undefined}
|
||||
columnWidths={columnWidths}
|
||||
renderers={{
|
||||
renderRow: rowRenderer,
|
||||
renderCell: cellRenderer,
|
||||
noRowsFallback: children,
|
||||
}}
|
||||
onScroll={onScroll}
|
||||
onSelectedCellChange={handleCellFocus}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
onColumnWidthsChange={setColumnWidths}
|
||||
/>
|
||||
<Activity mode={searchEnabled && searchOpen ? 'visible' : 'hidden'}>
|
||||
<GridSearchPanel
|
||||
ref={setSearchPanelRef}
|
||||
columnCount={columnsCount}
|
||||
isReadOnly={searchReadOnly}
|
||||
scrollToCell={scrollToCell}
|
||||
storage={searchStorage}
|
||||
open={searchOpen}
|
||||
onReplace={onReplace}
|
||||
onClose={handleSearchClose}
|
||||
onReplacingChange={handleReplacingChange}
|
||||
/>
|
||||
</Activity>
|
||||
</DataGridCellHeaderContext>
|
||||
</DataGridCellContext>
|
||||
</DataGridRowContext>
|
||||
</HeaderDnDContext>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -13,10 +13,16 @@ export interface IDataGridCellProps extends React.DetailedHTMLProps<React.HTMLAt
|
||||
isFocused: boolean;
|
||||
}
|
||||
|
||||
export interface IDataGridCellRenderer {
|
||||
export interface IDataGridCellRenderer {
|
||||
(propsOverride: Partial<IDataGridCellProps> & { [key: string]: any }): React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ICellChange {
|
||||
rowIdx: number;
|
||||
colIdx: number;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface IDataGridCellContext {
|
||||
cellElement?: IGridReactiveValue<
|
||||
React.ReactNode,
|
||||
@@ -25,8 +31,10 @@ export interface IDataGridCellContext {
|
||||
cell?: IGridReactiveValue<React.ReactNode, [rowIdx: number, colIdx: number]>;
|
||||
cellText?: IGridReactiveValue<string, [rowIdx: number, colIdx: number]>;
|
||||
cellTooltip?: IGridReactiveValue<string, [rowIdx: number, colIdx: number]>;
|
||||
getCellClassName?: IGridReactiveValue<string | undefined, [rowIdx: number, colIdx: number]>;
|
||||
getCellEditable?: (rowIdx: number, colIdx: number) => boolean;
|
||||
onCellChange?: (rowIdx: number, colIdx: number, value: any) => void;
|
||||
onCellChangeBatch?: (changes: ICellChange[]) => void;
|
||||
}
|
||||
|
||||
export const DataGridCellContext = createContext<IDataGridCellContext | null>(null);
|
||||
|
||||
@@ -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.
|
||||
@@ -19,3 +19,5 @@ export * from './formatters/BooleanFormatter.js';
|
||||
export * from './formatters/NumberFormatter.js';
|
||||
export * from './formatters/DateFormatter.js';
|
||||
export * from './formatters/BlobFormatter.js';
|
||||
|
||||
export * from './search/index.js';
|
||||
|
||||
+2
-2
@@ -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.
|
||||
@@ -88,7 +88,7 @@ export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer
|
||||
{...drop.props}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<span className="tw:overflow-hidden tw:text-ellipsis">{headerElement ?? getHeaderText ?? ''}</span>
|
||||
<span className="tw:overflow-hidden tw:text-ellipsis tw:w-full">{headerElement ?? getHeaderText ?? ''}</span>
|
||||
{isColumnSortable && onColumnSort && (
|
||||
<OrderButton sortState={sortingState} columnSortingMultiple={cellHeaderContext.columnSortingMultiple} onClick={handleSort} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 interface ISearchPatternOptions {
|
||||
caseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
regexp: boolean;
|
||||
}
|
||||
|
||||
export interface ICellMatch {
|
||||
rowIdx: number;
|
||||
colIdx: number;
|
||||
}
|
||||
|
||||
const ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;
|
||||
const WORD_BOUNDARY_REGEX = '\\b';
|
||||
const GLOBAL_FLAG_REGEX = 'g';
|
||||
const IGNORE_CASE_FLAG_REGEX = 'i';
|
||||
const ESCAPE_REPLACE_REGEX = '\\$&';
|
||||
|
||||
/** Build regex from query string + flags. Returns null for empty/invalid input. */
|
||||
export function buildSearchPattern(query: string, options: ISearchPatternOptions): RegExp | null {
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (options.regexp) {
|
||||
const flags = options.caseSensitive ? GLOBAL_FLAG_REGEX : GLOBAL_FLAG_REGEX + IGNORE_CASE_FLAG_REGEX;
|
||||
return new RegExp(query, flags);
|
||||
}
|
||||
|
||||
let escapedSearch = query.replace(ESCAPE_REGEX, ESCAPE_REPLACE_REGEX);
|
||||
if (options.wholeWord) {
|
||||
escapedSearch = `${WORD_BOUNDARY_REGEX}${escapedSearch}${WORD_BOUNDARY_REGEX}`;
|
||||
}
|
||||
|
||||
const flags = options.caseSensitive ? GLOBAL_FLAG_REGEX : GLOBAL_FLAG_REGEX + IGNORE_CASE_FLAG_REGEX;
|
||||
return new RegExp(escapedSearch, flags);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate all cells, return matches. O(rows * cols). */
|
||||
export function searchGrid(
|
||||
query: string,
|
||||
options: ISearchPatternOptions,
|
||||
rowCount: number,
|
||||
columnCount: number,
|
||||
getCellText: (rowIdx: number, colIdx: number) => string,
|
||||
): ICellMatch[] {
|
||||
const searchPattern = buildSearchPattern(query, options);
|
||||
|
||||
if (!searchPattern) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matches: ICellMatch[] = [];
|
||||
|
||||
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
|
||||
for (let colIdx = 0; colIdx < columnCount; colIdx++) {
|
||||
const cellText = getCellText(rowIdx, colIdx);
|
||||
if (searchPattern.test(cellText)) {
|
||||
matches.push({ rowIdx, colIdx });
|
||||
if (searchPattern.global) {
|
||||
searchPattern.lastIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/** Replace pattern in cell text. Returns new text and whether pattern still matches. */
|
||||
export function replaceInCell(cellText: string, pattern: RegExp, replaceValue: string): { newText: string; stillMatches: boolean } {
|
||||
const patternCopy = new RegExp(pattern);
|
||||
const newText = cellText.replace(patternCopy, replaceValue);
|
||||
|
||||
const stillMatches = newText.search(pattern) !== -1;
|
||||
|
||||
return { newText, stillMatches };
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, useContext, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import { clsx, SearchPanel, type SearchPanelRef } from '@dbeaver/ui-kit';
|
||||
|
||||
import { DataGridCellContext, type ICellChange } from '../DataGridCellContext.js';
|
||||
import { DataGridRowContext } from '../DataGridRowContext.js';
|
||||
import type { IGridReactiveValue } from '../IGridReactiveValue.js';
|
||||
import { useGridReactiveValue } from '../useGridReactiveValue.js';
|
||||
import { type IGridSearchStorage, useGridSearch } from './useGridSearch.js';
|
||||
|
||||
export interface GridSearchPanelRef {
|
||||
focus: () => void;
|
||||
refresh: () => void;
|
||||
getCellClassName: IGridReactiveValue<string | undefined, [number, number]>;
|
||||
}
|
||||
|
||||
interface GridSearchPanelProps {
|
||||
columnCount: number;
|
||||
scrollToCell: (rowIdx: number, colIdx: number) => void;
|
||||
onReplace: (updates: ICellChange[]) => void;
|
||||
onClose: () => void;
|
||||
onReplacingChange?: (isReplacing: boolean) => void;
|
||||
isReadOnly?: boolean;
|
||||
storage?: IGridSearchStorage;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export const GridSearchPanel = forwardRef<GridSearchPanelRef, GridSearchPanelProps>(function GridSearchPanel(
|
||||
{ columnCount, scrollToCell, onReplace, onClose, onReplacingChange, isReadOnly, storage, open },
|
||||
ref,
|
||||
) {
|
||||
const panelRef = useRef<SearchPanelRef>(null);
|
||||
const { cellText } = useContext(DataGridCellContext) ?? {};
|
||||
const rowContext = useContext(DataGridRowContext);
|
||||
const rowCount = useGridReactiveValue(rowContext?.rowCount);
|
||||
|
||||
const { snapshot, actions, getCellClassName, replaceOpen } = useGridSearch({
|
||||
rowCount: rowCount ?? 0,
|
||||
columnCount,
|
||||
cellText,
|
||||
scrollToCell,
|
||||
onReplace,
|
||||
onReplacingChange,
|
||||
storage,
|
||||
open,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
panelRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
focus: () => panelRef.current?.focus(),
|
||||
refresh: () => actions.refresh(),
|
||||
getCellClassName,
|
||||
}),
|
||||
[actions, getCellClassName],
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
actions.close();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchPanel
|
||||
ref={panelRef}
|
||||
className={clsx('rdg-search-panel tw:bg-(--theme-secondary)', isReadOnly && 'tw:px-0!')}
|
||||
isReadOnly={isReadOnly}
|
||||
query={{
|
||||
search: snapshot.query,
|
||||
replace: snapshot.replace,
|
||||
caseSensitive: snapshot.caseSensitive,
|
||||
wholeWord: snapshot.wholeWord,
|
||||
regexp: snapshot.regexp,
|
||||
}}
|
||||
defaultShowReplace={replaceOpen}
|
||||
searchMatchesCount={{
|
||||
count: snapshot.matchCount,
|
||||
current: snapshot.activeMatchIndex >= 0 ? snapshot.activeMatchIndex + 1 : 0,
|
||||
}}
|
||||
onQueryChange={actions.setQuery}
|
||||
onCaseSensitiveToggle={actions.toggleCaseSensitive}
|
||||
onWholeWordToggle={actions.toggleWholeWord}
|
||||
onRegexToggle={actions.toggleRegex}
|
||||
onFindNext={actions.findNext}
|
||||
onFindPrevious={actions.findPrevious}
|
||||
onReplaceChange={actions.setReplace}
|
||||
onReplaceToggle={actions.setReplaceOpen}
|
||||
onReplaceAll={actions.replaceAll}
|
||||
onReplaceNext={actions.replaceActive}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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 type CellKey = `${number}+${number}`;
|
||||
|
||||
export function makeCellKey(rowIdx: number, colIdx: number): CellKey {
|
||||
return `${rowIdx}+${colIdx}`;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { buildSearchPattern, searchGrid, replaceInCell, type ISearchPatternOptions, type ICellMatch } from './GridSearchEngine.js';
|
||||
export {
|
||||
useGridSearch,
|
||||
type IGridSearchSnapshot,
|
||||
type IGridSearchOptions,
|
||||
type IGridSearchStorageState,
|
||||
type IGridSearchStorage,
|
||||
} from './useGridSearch.js';
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import type { ICellMatch } from './GridSearchEngine.js';
|
||||
|
||||
export interface GridSearchQueryState {
|
||||
query: string;
|
||||
replace: string;
|
||||
caseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
regexp: boolean;
|
||||
}
|
||||
|
||||
export interface GridSearchMatchState {
|
||||
matchedCells: ICellMatch[];
|
||||
activeMatchIdx: number;
|
||||
}
|
||||
|
||||
export interface GridSearchUiState {
|
||||
replaceOpen: boolean;
|
||||
}
|
||||
|
||||
export interface GridSearchState extends GridSearchMatchState, GridSearchQueryState, GridSearchUiState { }
|
||||
|
||||
export type GridSearchReducerAction =
|
||||
| { type: 'SET_QUERY'; query: string }
|
||||
| { type: 'SET_REPLACE'; replace: string }
|
||||
| { type: 'TOGGLE_CASE_SENSITIVE' }
|
||||
| { type: 'TOGGLE_WHOLE_WORD' }
|
||||
| { type: 'TOGGLE_REGEX' }
|
||||
| { type: 'SET_MATCHES'; matchedCells: ICellMatch[]; preserveActiveIndex: boolean }
|
||||
| { type: 'SET_ACTIVE_MATCH'; index: number }
|
||||
| { type: 'REMOVE_MATCH'; index: number }
|
||||
| { type: 'SET_REPLACE_OPEN'; open: boolean };
|
||||
|
||||
export function computeActiveIdx(matchedCells: ICellMatch[], currentIdx: number, preserve: boolean): number {
|
||||
if (preserve && currentIdx >= 0 && currentIdx < matchedCells.length) {
|
||||
return currentIdx;
|
||||
}
|
||||
return matchedCells.length > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
export function computeActiveIdxAfterRemoval(matchCount: number, activeMatchIdx: number): number {
|
||||
if (matchCount === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(activeMatchIdx, matchCount - 1));
|
||||
}
|
||||
|
||||
export function computeActiveMatchIdx(matchCount: number, activeMatchIdx: number, offset = 0): number {
|
||||
if (matchCount === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (activeMatchIdx < 0 || activeMatchIdx >= matchCount) {
|
||||
return offset >= 0 ? 0 : matchCount - 1;
|
||||
}
|
||||
|
||||
const shifted = activeMatchIdx + offset;
|
||||
return ((shifted % matchCount) + matchCount) % matchCount;
|
||||
}
|
||||
|
||||
function areMatchesEqual(left: ICellMatch[], right: ICellMatch[]): boolean {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.some((left, i) => left.rowIdx !== right[i]!.rowIdx || left.colIdx !== right[i]!.colIdx)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function gridSearchReducer(state: GridSearchState, action: GridSearchReducerAction): GridSearchState {
|
||||
switch (action.type) {
|
||||
case 'SET_QUERY': {
|
||||
if (state.query === action.query) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (!action.query) {
|
||||
if (state.matchedCells.length === 0 && state.activeMatchIdx === -1) {
|
||||
return { ...state, query: action.query };
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
query: action.query,
|
||||
matchedCells: [],
|
||||
activeMatchIdx: -1,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...state, query: action.query };
|
||||
}
|
||||
case 'SET_REPLACE': {
|
||||
if (state.replace === action.replace) {
|
||||
return state;
|
||||
}
|
||||
return { ...state, replace: action.replace };
|
||||
}
|
||||
case 'TOGGLE_CASE_SENSITIVE':
|
||||
return { ...state, caseSensitive: !state.caseSensitive };
|
||||
case 'TOGGLE_WHOLE_WORD':
|
||||
return { ...state, wholeWord: !state.wholeWord };
|
||||
case 'TOGGLE_REGEX':
|
||||
return { ...state, regexp: !state.regexp };
|
||||
case 'SET_MATCHES': {
|
||||
const activeMatchIdx = computeActiveIdx(action.matchedCells, state.activeMatchIdx, action.preserveActiveIndex);
|
||||
|
||||
if (activeMatchIdx === state.activeMatchIdx && areMatchesEqual(state.matchedCells, action.matchedCells)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return { ...state, matchedCells: action.matchedCells, activeMatchIdx };
|
||||
}
|
||||
case 'SET_ACTIVE_MATCH': {
|
||||
if (state.matchedCells.length === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const boundedIndex = Math.max(0, Math.min(action.index, state.matchedCells.length - 1));
|
||||
if (boundedIndex === state.activeMatchIdx) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return { ...state, activeMatchIdx: boundedIndex };
|
||||
}
|
||||
case 'REMOVE_MATCH': {
|
||||
const newMatches = [...state.matchedCells];
|
||||
newMatches.splice(action.index, 1);
|
||||
const newActiveIdx = computeActiveIdxAfterRemoval(newMatches.length, state.activeMatchIdx);
|
||||
return { ...state, matchedCells: newMatches, activeMatchIdx: newActiveIdx };
|
||||
}
|
||||
case 'SET_REPLACE_OPEN': {
|
||||
if (state.replaceOpen === action.open) {
|
||||
return state;
|
||||
}
|
||||
return { ...state, replaceOpen: action.open };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
|
||||
import type { IGridReactiveValue } from '../IGridReactiveValue.js';
|
||||
import { buildSearchPattern, replaceInCell, searchGrid, type ICellMatch } from './GridSearchEngine.js';
|
||||
import type { ICellChange } from '../DataGridCellContext.js';
|
||||
import { makeCellKey } from './cellKey.js';
|
||||
import { useTrackedGridSearchCellSubscriptions } from './useTrackedGridSearchCellSubscriptions.js';
|
||||
import {
|
||||
computeActiveIdx,
|
||||
computeActiveIdxAfterRemoval,
|
||||
computeActiveMatchIdx,
|
||||
gridSearchReducer,
|
||||
type GridSearchQueryState,
|
||||
type GridSearchState,
|
||||
} from './state.js';
|
||||
|
||||
export type { ICellMatch } from './GridSearchEngine.js';
|
||||
|
||||
const DEFAULT_DEBOUNCE_MS = 300;
|
||||
|
||||
export interface IGridSearchStorageState extends GridSearchState {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export interface IGridSearchSnapshot extends GridSearchQueryState {
|
||||
matchCount: number;
|
||||
activeMatchIndex: number;
|
||||
}
|
||||
|
||||
export interface IGridSearchStorage {
|
||||
get(): IGridSearchStorageState | undefined;
|
||||
set(state: IGridSearchStorageState): void;
|
||||
update(state: Partial<IGridSearchStorageState>): void;
|
||||
}
|
||||
|
||||
const MATCH_CLASS = 'rdg-cell-search-match';
|
||||
const ACTIVE_CLASS = 'rdg-cell-search-match rdg-cell-search-active';
|
||||
|
||||
function scrollToMatch(scrollToCell: (rowIdx: number, colIdx: number) => void, match: ICellMatch | undefined): void {
|
||||
if (match) {
|
||||
scrollToCell(match.rowIdx, match.colIdx);
|
||||
}
|
||||
}
|
||||
|
||||
function getNextMatchAfterRemoval(matchedCells: ICellMatch[], activeMatchIdx: number): ICellMatch | undefined {
|
||||
const nextActiveIdx = computeActiveIdxAfterRemoval(matchedCells.length - 1, activeMatchIdx);
|
||||
if (nextActiveIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const actualIdx = nextActiveIdx >= activeMatchIdx ? nextActiveIdx + 1 : nextActiveIdx;
|
||||
return matchedCells[actualIdx];
|
||||
}
|
||||
|
||||
export interface IGridSearchActions {
|
||||
setQuery: (value: string) => void;
|
||||
setReplace: (value: string) => void;
|
||||
toggleCaseSensitive: () => void;
|
||||
toggleWholeWord: () => void;
|
||||
toggleRegex: () => void;
|
||||
findNext: () => void;
|
||||
findPrevious: () => void;
|
||||
replaceActive: () => void;
|
||||
replaceAll: () => void;
|
||||
setReplaceOpen: (open: boolean) => void;
|
||||
refresh: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface IGridSearchOptions {
|
||||
rowCount: number;
|
||||
columnCount: number;
|
||||
cellText?: IGridReactiveValue<string, [rowIdx: number, colIdx: number]>;
|
||||
scrollToCell: (rowIdx: number, colIdx: number) => void;
|
||||
onReplace: (updates: ICellChange[]) => void;
|
||||
onReplacingChange?: (isReplacing: boolean) => void;
|
||||
storage?: IGridSearchStorage;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export interface IGridSearchResult {
|
||||
snapshot: IGridSearchSnapshot;
|
||||
actions: IGridSearchActions;
|
||||
getCellClassName: IGridReactiveValue<string | undefined, [number, number]>;
|
||||
replaceOpen: boolean;
|
||||
}
|
||||
|
||||
function createInitialState(options: IGridSearchOptions): GridSearchState {
|
||||
const cached = options.storage?.get();
|
||||
const hasCache = cached !== undefined && !!cached.query && cached.matchedCells.length > 0;
|
||||
|
||||
return {
|
||||
query: cached?.query ?? '',
|
||||
replace: cached?.replace ?? '',
|
||||
caseSensitive: cached?.caseSensitive ?? false,
|
||||
wholeWord: cached?.wholeWord ?? false,
|
||||
regexp: cached?.regexp ?? false,
|
||||
replaceOpen: cached?.replaceOpen ?? false,
|
||||
matchedCells: hasCache ? cached!.matchedCells : [],
|
||||
activeMatchIdx: hasCache ? cached!.activeMatchIdx : -1,
|
||||
};
|
||||
}
|
||||
|
||||
function getCellText(options: IGridSearchOptions, rowIdx: number, colIdx: number): string {
|
||||
return options.cellText?.get(rowIdx, colIdx) ?? '';
|
||||
}
|
||||
|
||||
export function useGridSearch(options: IGridSearchOptions): IGridSearchResult {
|
||||
const [state, dispatch] = useReducer(gridSearchReducer, options, createInitialState);
|
||||
|
||||
const optionsRef = useRef(options);
|
||||
const stateRef = useRef(state);
|
||||
|
||||
const cellListenersRef = useRef(new Set<() => void>());
|
||||
const prevMatchedCellsRef = useRef(state.matchedCells);
|
||||
const [initialDerived] = useState(() => {
|
||||
const matchedSet = new Set(state.matchedCells.map(m => makeCellKey(m.rowIdx, m.colIdx)));
|
||||
const activeMatch = state.matchedCells[state.activeMatchIdx];
|
||||
const activeMatchKey = activeMatch ? makeCellKey(activeMatch.rowIdx, activeMatch.colIdx) : null;
|
||||
return { matchedSet, activeMatchKey };
|
||||
});
|
||||
const matchedSetRef = useRef(initialDerived.matchedSet);
|
||||
const activeMatchKeyRef = useRef(initialDerived.activeMatchKey);
|
||||
const queryRef = useRef(state.query);
|
||||
|
||||
const lastGridSizeRef = useRef({ rowCount: options.rowCount, columnCount: options.columnCount });
|
||||
const isInitialMountRef = useRef(true);
|
||||
|
||||
const runSearchAndScroll = useCallback((searchState: Omit<GridSearchQueryState, 'replace'>, preserveActiveIndex: boolean): void => {
|
||||
const opts = optionsRef.current;
|
||||
const currentState = stateRef.current;
|
||||
|
||||
const matches = searchGrid(
|
||||
searchState.query,
|
||||
{
|
||||
caseSensitive: searchState.caseSensitive,
|
||||
wholeWord: searchState.wholeWord,
|
||||
regexp: searchState.regexp,
|
||||
},
|
||||
opts.rowCount,
|
||||
opts.columnCount,
|
||||
(rowIdx, colIdx) => getCellText(opts, rowIdx, colIdx),
|
||||
);
|
||||
|
||||
dispatch({ type: 'SET_MATCHES', matchedCells: matches, preserveActiveIndex });
|
||||
|
||||
const newActiveIdx = computeActiveIdx(matches, currentState.activeMatchIdx, preserveActiveIndex);
|
||||
scrollToMatch(opts.scrollToCell, matches[newActiveIdx]);
|
||||
}, []);
|
||||
|
||||
const handleTrackedCellChange = useCallback((): void => {
|
||||
const st = stateRef.current;
|
||||
if (!st.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
runSearchAndScroll(st, true);
|
||||
}, [runSearchAndScroll]);
|
||||
|
||||
const { syncGridSubscriptions, clearTrackedCellSubscriptions } = useTrackedGridSearchCellSubscriptions({
|
||||
debounceMs: DEFAULT_DEBOUNCE_MS,
|
||||
onTrackedCellChange: handleTrackedCellChange,
|
||||
});
|
||||
|
||||
const actions = useMemo<IGridSearchActions>(() => {
|
||||
function runSearchNow(preserveActiveIndex: boolean): void {
|
||||
runSearchAndScroll(stateRef.current, preserveActiveIndex);
|
||||
}
|
||||
|
||||
return {
|
||||
setQuery(value: string): void {
|
||||
dispatch({ type: 'SET_QUERY', query: value });
|
||||
},
|
||||
|
||||
setReplace(value: string): void {
|
||||
dispatch({ type: 'SET_REPLACE', replace: value });
|
||||
},
|
||||
|
||||
toggleCaseSensitive(): void {
|
||||
dispatch({ type: 'TOGGLE_CASE_SENSITIVE' });
|
||||
},
|
||||
|
||||
toggleWholeWord(): void {
|
||||
dispatch({ type: 'TOGGLE_WHOLE_WORD' });
|
||||
},
|
||||
|
||||
toggleRegex(): void {
|
||||
dispatch({ type: 'TOGGLE_REGEX' });
|
||||
},
|
||||
|
||||
findNext(): void {
|
||||
const st = stateRef.current;
|
||||
if (st.matchedCells.length === 0) {
|
||||
if (st.query) {
|
||||
runSearchNow(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const nextIdx = computeActiveMatchIdx(st.matchedCells.length, st.activeMatchIdx, 1);
|
||||
dispatch({ type: 'SET_ACTIVE_MATCH', index: nextIdx });
|
||||
scrollToMatch(optionsRef.current.scrollToCell, st.matchedCells[nextIdx]);
|
||||
},
|
||||
|
||||
findPrevious(): void {
|
||||
const st = stateRef.current;
|
||||
if (st.matchedCells.length === 0) {
|
||||
if (st.query) {
|
||||
runSearchNow(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const prevIdx = computeActiveMatchIdx(st.matchedCells.length, st.activeMatchIdx, -1);
|
||||
dispatch({ type: 'SET_ACTIVE_MATCH', index: prevIdx });
|
||||
scrollToMatch(optionsRef.current.scrollToCell, st.matchedCells[prevIdx]);
|
||||
},
|
||||
|
||||
replaceActive(): void {
|
||||
const st = stateRef.current;
|
||||
if (st.activeMatchIdx < 0 || st.activeMatchIdx >= st.matchedCells.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = st.matchedCells[st.activeMatchIdx];
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = buildSearchPattern(st.query, {
|
||||
caseSensitive: st.caseSensitive,
|
||||
wholeWord: st.wholeWord,
|
||||
regexp: st.regexp,
|
||||
});
|
||||
if (!pattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = optionsRef.current;
|
||||
opts.onReplacingChange?.(true);
|
||||
try {
|
||||
const cellValue = getCellText(opts, match.rowIdx, match.colIdx);
|
||||
const { newText, stillMatches } = replaceInCell(cellValue, pattern, st.replace);
|
||||
opts.onReplace([{ rowIdx: match.rowIdx, colIdx: match.colIdx, value: newText }]);
|
||||
|
||||
if (!stillMatches) {
|
||||
dispatch({ type: 'REMOVE_MATCH', index: st.activeMatchIdx });
|
||||
|
||||
const nextMatch = getNextMatchAfterRemoval(st.matchedCells, st.activeMatchIdx);
|
||||
scrollToMatch(opts.scrollToCell, nextMatch);
|
||||
}
|
||||
} finally {
|
||||
opts.onReplacingChange?.(false);
|
||||
}
|
||||
},
|
||||
|
||||
replaceAll(): void {
|
||||
const st = stateRef.current;
|
||||
if (st.matchedCells.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = buildSearchPattern(st.query, {
|
||||
caseSensitive: st.caseSensitive,
|
||||
wholeWord: st.wholeWord,
|
||||
regexp: st.regexp,
|
||||
});
|
||||
if (!pattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = optionsRef.current;
|
||||
opts.onReplacingChange?.(true);
|
||||
try {
|
||||
const matches = [...st.matchedCells];
|
||||
const updates = matches.map(match => {
|
||||
const cellValue = getCellText(opts, match.rowIdx, match.colIdx);
|
||||
const { newText } = replaceInCell(cellValue, pattern, st.replace);
|
||||
return { rowIdx: match.rowIdx, colIdx: match.colIdx, value: newText };
|
||||
});
|
||||
opts.onReplace(updates);
|
||||
} finally {
|
||||
opts.onReplacingChange?.(false);
|
||||
}
|
||||
|
||||
runSearchNow(false);
|
||||
},
|
||||
|
||||
setReplaceOpen(open: boolean): void {
|
||||
dispatch({ type: 'SET_REPLACE_OPEN', open });
|
||||
},
|
||||
|
||||
refresh(): void {
|
||||
if (stateRef.current.query) {
|
||||
runSearchNow(true);
|
||||
}
|
||||
},
|
||||
|
||||
close(): void {
|
||||
const opts = optionsRef.current;
|
||||
const st = stateRef.current;
|
||||
// Sync state to storage explicitly (cannot rely on effect before potential unmount)
|
||||
opts.storage?.set({
|
||||
matchedCells: st.matchedCells,
|
||||
activeMatchIdx: st.activeMatchIdx,
|
||||
query: st.query,
|
||||
replace: st.replace,
|
||||
caseSensitive: st.caseSensitive,
|
||||
wholeWord: st.wholeWord,
|
||||
regexp: st.regexp,
|
||||
replaceOpen: st.replaceOpen,
|
||||
open: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [runSearchAndScroll]);
|
||||
|
||||
const [getCellClassName] = useState<IGridReactiveValue<string | undefined, [number, number]>>(() => ({
|
||||
get(rowIdx: number, colIdx: number): string | undefined {
|
||||
if (!queryRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = makeCellKey(rowIdx, colIdx);
|
||||
if (!matchedSetRef.current.has(key)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (key === activeMatchKeyRef.current) {
|
||||
return ACTIVE_CLASS;
|
||||
}
|
||||
|
||||
return MATCH_CLASS;
|
||||
},
|
||||
subscribe(onChange: () => void): () => void {
|
||||
cellListenersRef.current.add(onChange);
|
||||
return () => {
|
||||
cellListenersRef.current.delete(onChange);
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
// --- Sync React state → refs, notify cell listeners ---
|
||||
useEffect(() => {
|
||||
optionsRef.current = options;
|
||||
stateRef.current = state;
|
||||
|
||||
// Only rebuild matchedSet when matchedCells identity changes
|
||||
if (state.matchedCells !== prevMatchedCellsRef.current) {
|
||||
matchedSetRef.current = new Set(state.matchedCells.map(m => makeCellKey(m.rowIdx, m.colIdx)));
|
||||
prevMatchedCellsRef.current = state.matchedCells;
|
||||
}
|
||||
|
||||
const activeMatch = state.matchedCells[state.activeMatchIdx];
|
||||
activeMatchKeyRef.current = activeMatch ? makeCellKey(activeMatch.rowIdx, activeMatch.colIdx) : null;
|
||||
queryRef.current = state.query;
|
||||
|
||||
for (const listener of cellListenersRef.current) {
|
||||
listener();
|
||||
}
|
||||
}, [options, state]);
|
||||
|
||||
// --- Storage sync ---
|
||||
useEffect(() => {
|
||||
optionsRef.current.storage?.set({
|
||||
matchedCells: state.matchedCells,
|
||||
activeMatchIdx: state.activeMatchIdx,
|
||||
query: state.query,
|
||||
replace: state.replace,
|
||||
caseSensitive: state.caseSensitive,
|
||||
wholeWord: state.wholeWord,
|
||||
regexp: state.regexp,
|
||||
replaceOpen: state.replaceOpen,
|
||||
open: optionsRef.current.open ?? true,
|
||||
});
|
||||
}, [
|
||||
state.query,
|
||||
state.replace,
|
||||
state.caseSensitive,
|
||||
state.wholeWord,
|
||||
state.regexp,
|
||||
state.replaceOpen,
|
||||
state.matchedCells,
|
||||
state.activeMatchIdx,
|
||||
options.open,
|
||||
]);
|
||||
|
||||
// --- Debounced search (reacts to query/flag changes) ---
|
||||
useEffect(() => {
|
||||
if (!state.query) {
|
||||
clearTrackedCellSubscriptions();
|
||||
return;
|
||||
}
|
||||
|
||||
syncGridSubscriptions(options.cellText, options.rowCount, options.columnCount);
|
||||
}, [state.query, options.cellText, options.rowCount, options.columnCount, syncGridSubscriptions, clearTrackedCellSubscriptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMountRef.current) {
|
||||
isInitialMountRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.query) {
|
||||
clearTrackedCellSubscriptions();
|
||||
return;
|
||||
}
|
||||
|
||||
const searchState = {
|
||||
query: state.query,
|
||||
caseSensitive: state.caseSensitive,
|
||||
wholeWord: state.wholeWord,
|
||||
regexp: state.regexp,
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
runSearchAndScroll(searchState, false);
|
||||
}, DEFAULT_DEBOUNCE_MS);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [state.query, state.caseSensitive, state.wholeWord, state.regexp, runSearchAndScroll, clearTrackedCellSubscriptions]);
|
||||
|
||||
// --- Re-search when grid size changes ---
|
||||
useEffect(() => {
|
||||
if (lastGridSizeRef.current.rowCount !== options.rowCount || lastGridSizeRef.current.columnCount !== options.columnCount) {
|
||||
lastGridSizeRef.current = { rowCount: options.rowCount, columnCount: options.columnCount };
|
||||
actions.refresh();
|
||||
}
|
||||
}, [options.rowCount, options.columnCount, actions]);
|
||||
|
||||
// --- Initial search on mount ---
|
||||
useEffect(() => {
|
||||
const st = stateRef.current;
|
||||
|
||||
// Cache was restored with matches — cell listeners will be notified by the state→ref sync effect
|
||||
if (st.matchedCells.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!st.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.refresh();
|
||||
}, [actions]);
|
||||
|
||||
// --- Cleanup ---
|
||||
useEffect(
|
||||
() => () => {
|
||||
cellListenersRef.current.clear();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const snapshot = useMemo<IGridSearchSnapshot>(
|
||||
() => ({
|
||||
query: state.query,
|
||||
replace: state.replace,
|
||||
caseSensitive: state.caseSensitive,
|
||||
wholeWord: state.wholeWord,
|
||||
regexp: state.regexp,
|
||||
matchCount: state.matchedCells.length,
|
||||
activeMatchIndex: state.activeMatchIdx,
|
||||
}),
|
||||
[state.query, state.replace, state.caseSensitive, state.wholeWord, state.regexp, state.matchedCells, state.activeMatchIdx],
|
||||
);
|
||||
|
||||
return { snapshot, actions, getCellClassName, replaceOpen: state.replaceOpen };
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import type { IGridReactiveValue } from '../IGridReactiveValue.js';
|
||||
|
||||
export interface IUseTrackedGridSearchCellSubscriptionsOptions {
|
||||
debounceMs: number;
|
||||
onTrackedCellChange: () => void;
|
||||
}
|
||||
|
||||
export interface IUseTrackedGridSearchCellSubscriptionsResult {
|
||||
syncGridSubscriptions: (cellText: IGridReactiveValue<string, [number, number]> | undefined, rowCount: number, columnCount: number) => void;
|
||||
clearTrackedCellSubscriptions: () => void;
|
||||
}
|
||||
|
||||
export function useTrackedGridSearchCellSubscriptions({
|
||||
debounceMs,
|
||||
onTrackedCellChange,
|
||||
}: IUseTrackedGridSearchCellSubscriptionsOptions): IUseTrackedGridSearchCellSubscriptionsResult {
|
||||
const trackedCellUnsubscribesRef = useRef<Array<() => void>>([]);
|
||||
const searchRerunTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const subscriptionSourceRef = useRef<IGridReactiveValue<string, [number, number]> | undefined>(undefined);
|
||||
const subscriptionSizeRef = useRef({ rowCount: -1, columnCount: -1 });
|
||||
|
||||
const clearTrackedCellSubscriptions = useCallback((): void => {
|
||||
if (searchRerunTimeoutRef.current !== null) {
|
||||
clearTimeout(searchRerunTimeoutRef.current);
|
||||
searchRerunTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
for (const unsubscribe of trackedCellUnsubscribesRef.current) {
|
||||
unsubscribe();
|
||||
}
|
||||
|
||||
trackedCellUnsubscribesRef.current = [];
|
||||
subscriptionSourceRef.current = undefined;
|
||||
subscriptionSizeRef.current = { rowCount: -1, columnCount: -1 };
|
||||
}, []);
|
||||
|
||||
const scheduleTrackedCellSearchRerun = useCallback((): void => {
|
||||
if (searchRerunTimeoutRef.current !== null) {
|
||||
clearTimeout(searchRerunTimeoutRef.current);
|
||||
}
|
||||
|
||||
searchRerunTimeoutRef.current = setTimeout(() => {
|
||||
searchRerunTimeoutRef.current = null;
|
||||
onTrackedCellChange();
|
||||
}, debounceMs);
|
||||
}, [debounceMs, onTrackedCellChange]);
|
||||
|
||||
const syncGridSubscriptions = useCallback(
|
||||
(cellText: IGridReactiveValue<string, [number, number]> | undefined, rowCount: number, columnCount: number): void => {
|
||||
const sourceChanged = subscriptionSourceRef.current !== cellText;
|
||||
const sizeChanged = subscriptionSizeRef.current.rowCount !== rowCount || subscriptionSizeRef.current.columnCount !== columnCount;
|
||||
|
||||
if (!sourceChanged && !sizeChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTrackedCellSubscriptions();
|
||||
subscriptionSourceRef.current = cellText;
|
||||
subscriptionSizeRef.current = { rowCount, columnCount };
|
||||
|
||||
if (!cellText) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Current implementation performs full-grid search (rows * cols),
|
||||
// so we subscribe to all cells in the current grid.
|
||||
// If partial/range search is introduced later, this should be revised.
|
||||
const unsubscribes: Array<() => void> = [];
|
||||
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
|
||||
for (let colIdx = 0; colIdx < columnCount; colIdx++) {
|
||||
unsubscribes.push(cellText.subscribe(scheduleTrackedCellSearchRerun, rowIdx, colIdx));
|
||||
}
|
||||
}
|
||||
|
||||
trackedCellUnsubscribesRef.current = unsubscribes;
|
||||
},
|
||||
[clearTrackedCellSubscriptions, scheduleTrackedCellSearchRerun],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
clearTrackedCellSubscriptions();
|
||||
},
|
||||
[clearTrackedCellSubscriptions],
|
||||
);
|
||||
|
||||
return {
|
||||
syncGridSubscriptions,
|
||||
clearTrackedCellSubscriptions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { ICellChange } from './DataGridCellContext.js';
|
||||
import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
import type { GridSearchPanelRef } from './search/GridSearchPanel.js';
|
||||
import type { IGridSearchStorage } from './search/useGridSearch.js';
|
||||
|
||||
interface UseDataGridSearchOptions {
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
searchStorage: IGridSearchStorage | undefined;
|
||||
getCellEditable: ((rowIdx: number, colIdx: number) => boolean) | undefined;
|
||||
onCellChangeBatch: ((updates: ICellChange[]) => void) | undefined;
|
||||
}
|
||||
|
||||
export function useDataGridSearch({ containerRef, searchStorage, getCellEditable, onCellChangeBatch }: UseDataGridSearchOptions): {
|
||||
searchOpen: boolean;
|
||||
searchCellClassName: IGridReactiveValue<string | undefined, [number, number]> | undefined;
|
||||
searchPanelRef: React.RefObject<GridSearchPanelRef | null>;
|
||||
setSearchPanelRef: (instance: GridSearchPanelRef | null) => void;
|
||||
isReplacingRef: React.RefObject<boolean>;
|
||||
handleSearchOpen: () => void;
|
||||
handleSearchClose: () => void;
|
||||
onReplace: (updates: { rowIdx: number; colIdx: number; value: string }[]) => void;
|
||||
handleReplacingChange: (value: boolean) => void;
|
||||
} {
|
||||
const searchPanelRef = useRef<GridSearchPanelRef | null>(null);
|
||||
const isReplacingRef = useRef<boolean>(false);
|
||||
|
||||
const [searchOpen, setSearchOpen] = useState(() => searchStorage?.get()?.open ?? false);
|
||||
const [searchCellClassName, setSearchCellClassName] = useState<IGridReactiveValue<string | undefined, [number, number]>>();
|
||||
|
||||
const setSearchPanelRef = useCallback((instance: GridSearchPanelRef | null) => {
|
||||
searchPanelRef.current = instance;
|
||||
setSearchCellClassName(instance?.getCellClassName);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyF') {
|
||||
e.preventDefault();
|
||||
setSearchOpen(true);
|
||||
searchPanelRef.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener('keydown', handleKeyDown);
|
||||
return () => el.removeEventListener('keydown', handleKeyDown);
|
||||
}, [containerRef]);
|
||||
|
||||
function handleSearchOpen() {
|
||||
setSearchOpen(true);
|
||||
searchPanelRef.current?.focus();
|
||||
}
|
||||
|
||||
function handleSearchClose() {
|
||||
setSearchOpen(false);
|
||||
containerRef.current?.querySelector<HTMLDivElement>('[aria-selected="true"]')?.focus();
|
||||
}
|
||||
|
||||
function onReplace(updates: { rowIdx: number; colIdx: number; value: string }[]) {
|
||||
const validUpdates = updates.filter(u => getCellEditable?.(u.rowIdx, u.colIdx) !== false);
|
||||
if (validUpdates.length > 0) {
|
||||
onCellChangeBatch?.(validUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReplacingChange(value: boolean) {
|
||||
isReplacingRef.current = value;
|
||||
}
|
||||
|
||||
return {
|
||||
searchOpen,
|
||||
searchCellClassName,
|
||||
searchPanelRef,
|
||||
setSearchPanelRef,
|
||||
isReplacingRef,
|
||||
handleSearchOpen,
|
||||
handleSearchClose,
|
||||
onReplace,
|
||||
handleReplacingChange,
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
@@ -10,5 +10,5 @@ import { WebSocket } from 'ws';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
// different machine has its own timezones and some tests can fail because of it
|
||||
process.env.TZ = 'UTC';
|
||||
process.env['TZ'] = 'UTC';
|
||||
globalThis.WebSocket = WebSocket as any;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user