Merge remote-tracking branch 'origin/devel' into feat/CB-2302/dnd-folders

This commit is contained in:
Wroud
2022-08-29 14:36:20 +03:00
76 changed files with 592 additions and 295 deletions
+31
View File
@@ -0,0 +1,31 @@
name: Run Jest Tests
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "devel" branch
pull_request:
branches: [ "devel" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
run-jest-tests:
# The type of runner that the job will run on
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./webapp
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install --legacy-peer-deps
- run: npm install lerna -g
- run: lerna bootstrap
- run: lerna run test --no-bail --stream -- --reporters default github-actions
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true
Bundle-Version: 1.0.11.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.12.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
+1 -1
View File
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.model</artifactId>
<version>1.0.11-SNAPSHOT</version>
<version>1.0.12-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -67,6 +67,7 @@ public abstract class AbstractSessionFederated implements SMSessionFederated {
return parentSession.getSingletonProject();
}
@Override
public void close() {
// do nothing
}
@@ -24,12 +24,14 @@ import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBIcon;
import org.jkiss.dbeaver.model.DBPImage;
import org.jkiss.dbeaver.model.DBPObject;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.SMSessionContext;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.rm.RMController;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.model.rm.RMResource;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.utils.CommonUtils;
import java.util.ArrayList;
import java.util.List;
@@ -114,6 +116,21 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode {
return getNodeName();
}
@Override
public DBPProject getOwnerProject() {
List<? extends DBPProject> globalProjects = getModel().getModelProjects();
if (globalProjects == null) {
return null;
}
for (DBPProject modelProject : globalProjects) {
if (CommonUtils.equalObjects(modelProject.getId(), project.getId())) {
return modelProject;
}
}
return null;
}
@Nullable
@Override
public DBPObject getObjectDetails(@NotNull DBRProgressMonitor monitor, @NotNull SMSessionContext sessionContext, @NotNull Object dataSource) throws DBException {
@@ -24,6 +24,7 @@ import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBIcon;
import org.jkiss.dbeaver.model.DBPImage;
import org.jkiss.dbeaver.model.DBPObject;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.SMSessionContext;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.rm.RMController;
@@ -165,6 +166,11 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode {
return resource;
}
@Override
public DBPProject getOwnerProject() {
return getParentNode().getOwnerProject();
}
public RMResource getResource() {
return resource;
}
@@ -545,7 +545,7 @@ public class LocalResourceController implements RMController {
if (Files.exists(path)) {
try {
project.setCreateTime(
OffsetDateTime.ofInstant(Files.getLastModifiedTime(path).toInstant(), ZoneId.of("UTC")));
OffsetDateTime.ofInstant(Files.getLastModifiedTime(path).toInstant(), ZoneId.of("UTC")).toInstant().toEpochMilli());
} catch (IOException e) {
log.error(e);
}
@@ -584,14 +584,19 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
}
}
public void close() throws DBException {
@Override
public void close() {
try {
resetNavigationModel();
resetSessionCache();
} catch (Throwable e) {
log.error(e);
}
clearAuthTokens();
try {
clearAuthTokens();
} catch (Exception e) {
log.error("Error closing web session tokens");
}
this.sessionAuthContext.close();
this.userContext.setUser(null);
@@ -599,6 +604,7 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
this.defaultProject.dispose();
this.defaultProject = null;
}
super.close();
}
private void clearAuthTokens() throws DBException {
@@ -959,6 +965,11 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
return userContext.getActiveUserCredentials();
}
@Override
public void refreshSMSession() throws DBException {
userContext.refreshSMSession();
}
public VirtualProjectImpl getProjectById(@Nullable String projectId) {
if (projectId == null) {
return defaultProject;
@@ -47,6 +47,7 @@ public class WebUserContext implements SMCredentialsProvider {
private Set<String> userPermissions;
private SMCredentials smCredentials = null;
private String smSessionId;
private String refreshToken;
private SMController securityController;
private SMAdminController adminSecurityController;
@@ -64,7 +65,7 @@ public class WebUserContext implements SMCredentialsProvider {
* @param smAuthInfo - auth info from security manager
* @throws DBException - if user already authorized and new token come from another user
*/
public void refresh(SMAuthInfo smAuthInfo) throws DBException {
public synchronized void refresh(SMAuthInfo smAuthInfo) throws DBException {
if (smAuthInfo.getAuthStatus() != SMAuthStatus.SUCCESS) {
throw new DBCException("Authorization did not complete successfully");
}
@@ -77,7 +78,12 @@ public class WebUserContext implements SMCredentialsProvider {
if (isNonAnonymousUserAuthorized && isSessionChanged && !Objects.equals(getUserId(), authPermissions.getUserId())) {
throw new DBCException("Another user is already logged in");
}
this.smCredentials = new SMCredentials(smAuthInfo.getSmAuthToken(), authPermissions.getUserId(), authPermissions.getPermissions());
this.smCredentials = new SMCredentials(
smAuthInfo.getSmAuthToken(),
authPermissions.getUserId(),
authPermissions.getPermissions()
);
this.refreshToken = smAuthInfo.getSmRefreshToken();
this.userPermissions = authPermissions.getPermissions();
this.securityController = application.getSecurityController(this);
this.adminSecurityController = application.getAdminSecurityController(this);
@@ -89,10 +95,23 @@ public class WebUserContext implements SMCredentialsProvider {
}
public synchronized void refreshSMSession() throws DBException {
if (smCredentials == null || refreshToken == null) {
return;
}
var newTokens = securityController.refreshSession(refreshToken);
this.refreshToken = newTokens.getSmRefreshToken();
this.smCredentials = new SMCredentials(
newTokens.getSmAccessToken(),
smCredentials.getUserId(),
smCredentials.getPermissions()
);
}
/**
* reset the state as if the user is not logged in
*/
public void reset() throws DBException {
public synchronized void reset() throws DBException {
if (this.smCredentials != null) {
this.securityController.logout();
}
@@ -155,8 +174,8 @@ public class WebUserContext implements SMCredentialsProvider {
}
if (userPermissions != null &&
!userPermissions.isEmpty() &&
!userPermissions.contains(DBWConstants.PERMISSION_PUBLIC))
{
!userPermissions.contains(DBWConstants.PERMISSION_PUBLIC)
) {
userPermissions.add(DBWConstants.PERMISSION_PUBLIC);
}
}
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
Bundle-Version: 22.1.5.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 22.2.0.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.product.ce</artifactId>
<version>22.1.5-SNAPSHOT</version>
<version>22.2.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Base JDBC drivers
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
Bundle-Version: 1.0.56.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.57.qualifier
Bundle-Release-Date: 20220905
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.56-SNAPSHOT</version>
<version>1.0.57-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
Bundle-Version: 22.1.5.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 22.2.0.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator
+1 -1
View File
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.server</artifactId>
<version>22.1.5-SNAPSHOT</version>
<version>22.2.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -181,11 +181,7 @@ public class WebSessionManager {
for (WebSession session : expiredList) {
log.debug("> Expire session '" + session.getSessionId() + "'");
try {
session.close();
} catch (DBException e) {
log.debug("Failed close session:" + session.getSessionId(), e);
}
session.close();
}
}
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
Bundle-Version: 1.0.55.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.56.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.admin</artifactId>
<version>1.0.55-SNAPSHOT</version>
<version>1.0.56-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true
Bundle-Version: 1.0.55.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.56.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.auth</artifactId>
<version>1.0.55-SNAPSHOT</version>
<version>1.0.56-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true
Bundle-Version: 1.0.56.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.57.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.data.transfer</artifactId>
<version>1.0.56-SNAPSHOT</version>
<version>1.0.57-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true
Bundle-Version: 1.0.59.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.60.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.metadata</artifactId>
<version>1.0.59-SNAPSHOT</version>
<version>1.0.60-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: %Bundle-Vendor
Bundle-Name: %Bundle-Name
Bundle-SymbolicName: io.cloudbeaver.service.rm;singleton:=true
Bundle-Version: 1.0.8.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.9.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.rm</artifactId>
<version>1.0.8-SNAPSHOT</version>
<version>1.0.9-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Security Sevice
Bundle-Vendor: DBeaver Corp
Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true
Bundle-Version: 1.0.11.qualifier
Bundle-Release-Date: 20220822
Bundle-Version: 1.0.12.qualifier
Bundle-Release-Date: 20220905
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -238,12 +238,14 @@ CREATE TABLE CB_SESSION_LOG
CREATE TABLE CB_AUTH_TOKEN
(
TOKEN_ID VARCHAR(128) NOT NULL,
SESSION_ID VARCHAR(64) NOT NULL,
USER_ID VARCHAR(128),
TOKEN_ID VARCHAR(128) NOT NULL,
REFRESH_TOKEN_ID VARCHAR(128),
SESSION_ID VARCHAR(64) NOT NULL,
USER_ID VARCHAR(128),
EXPIRATION_TIME TIMESTAMP NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
EXPIRATION_TIME TIMESTAMP NOT NULL,
REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (TOKEN_ID),
FOREIGN KEY (SESSION_ID) REFERENCES CB_SESSION (SESSION_ID) ON DELETE CASCADE,
@@ -0,0 +1,5 @@
ALTER TABLE CB_AUTH_TOKEN
ADD REFRESH_TOKEN_ID VARCHAR(128);
ALTER TABLE CB_AUTH_TOKEN
ADD REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.security</artifactId>
<version>1.0.11-SNAPSHOT</version>
<version>1.0.12-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -76,4 +76,9 @@ public class LocalAuthSession implements SMSession {
return webSession.getSingletonProject();
}
@Override
public void close() {
}
}
@@ -27,6 +27,7 @@ import io.cloudbeaver.model.app.WebAuthConfiguration;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.service.security.db.CBDatabase;
import io.cloudbeaver.service.security.internal.AuthAttemptSessionInfo;
import io.cloudbeaver.service.security.internal.RefreshTokenInfo;
import io.cloudbeaver.utils.WebAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -39,7 +40,9 @@ import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCTransaction;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.model.security.*;
import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException;
import org.jkiss.dbeaver.model.security.exception.SMException;
import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException;
import org.jkiss.dbeaver.model.security.user.SMAuthPermissions;
import org.jkiss.dbeaver.model.security.user.SMObjectPermissions;
import org.jkiss.dbeaver.model.security.user.SMRole;
@@ -65,7 +68,8 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
private static final Log log = Log.getLog(CBEmbeddedSecurityController.class);
private static final int TOKEN_HOURS_ALIVE_TIME = 1;
private static final int ACCESS_TOKEN_TTL_IN_MINUTES = 20;
private static final int REFRESH_TOKEN_TTL_IN_HOURS = 24;
protected static final String CHAR_BOOL_TRUE = "Y";
protected static final String CHAR_BOOL_FALSE = "N";
@@ -844,10 +848,15 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
try (Connection dbCon = database.openConnection()) {
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
var smSessionId = createSmSession(appSessionId, null, sessionParameters, sessionType, dbCon);
var token = generateAuthToken(smSessionId, null, dbCon);
var smTokens = generateNewSessionToken(smSessionId, null, dbCon);
var permissions = getAnonymousUserPermissions();
txn.commit();
return SMAuthInfo.success(UUID.randomUUID().toString(), token, new SMAuthPermissions(null, smSessionId, permissions), Map.of());
return SMAuthInfo.success(
UUID.randomUUID().toString(),
smTokens.getSmAccessToken(),
smTokens.getSmRefreshToken(),
new SMAuthPermissions(null, smSessionId, permissions), Map.of()
);
}
} catch (SQLException e) {
throw new DBException(e.getMessage(), e);
@@ -1104,9 +1113,15 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
}
}
String smToken = findTokenBySmSession(smSessionId);
SMAuthPermissions authPermissions = getTokenPermissions(smToken);
var successAuthStatus = SMAuthInfo.success(authId, smToken, authPermissions, authData);
SMTokens smTokens = findTokenBySmSession(smSessionId);
SMAuthPermissions authPermissions = getTokenPermissions(smTokens.getSmAccessToken());
var successAuthStatus = SMAuthInfo.success(
authId,
smTokens.getSmAccessToken(),
smTokens.getSmRefreshToken(),
authPermissions,
authData
);
updateAuthStatus(authId, SMAuthStatus.EXPIRED, authData, null, authPermissions.getSessionId());
return successAuthStatus;
@@ -1118,10 +1133,29 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
@Override
public void logout() throws DBException {
var currentUserCreds = getCurrentUserCreds();
invalidateUserSession(currentUserCreds.getSmToken());
invalidateUserTokens(currentUserCreds.getSmToken());
}
private void invalidateUserSession(String smToken) throws DBCException {
@Override
public SMTokens refreshSession(@NotNull String refreshToken) throws DBException {
var currentUserCreds = getCurrentUserCreds();
var currentUserAccessToken = currentUserCreds.getSmToken();
var expectedRefreshTokenInfo = findRefreshToken(currentUserAccessToken);
if (!expectedRefreshTokenInfo.getRefreshToken().equals(refreshToken)) {
throw new SMException("Invalid refresh token");
}
try (var dbCon = database.openConnection()) {
invalidateUserTokens(currentUserAccessToken);
return generateNewSessionToken(expectedRefreshTokenInfo.getSessionId(), expectedRefreshTokenInfo.getUserId(), dbCon);
} catch (SQLException e) {
throw new DBException("Error refreshing sm session", e);
}
}
private void invalidateUserTokens(String smToken) throws DBCException {
try (Connection dbCon = database.openConnection()) {
JDBCUtils.executeStatement(dbCon, "DELETE FROM CB_AUTH_TOKEN WHERE TOKEN_ID=?", smToken);
} catch (SQLException e) {
@@ -1137,16 +1171,41 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
return currentUserCreds;
}
private String findTokenBySmSession(String smSessionId) throws DBException {
private SMTokens findTokenBySmSession(String smSessionId) throws DBException {
try (Connection dbCon = database.openConnection();
PreparedStatement dbStat = dbCon.prepareStatement("SELECT TOKEN_ID FROM CB_AUTH_TOKEN WHERE SESSION_ID=?");
PreparedStatement dbStat = dbCon.prepareStatement("SELECT TOKEN_ID, REFRESH_TOKEN_ID FROM CB_AUTH_TOKEN WHERE SESSION_ID=?")
) {
dbStat.setString(1, smSessionId);
try (var dbResult = dbStat.executeQuery()) {
if (!dbResult.next()) {
throw new SMException("Token not found");
}
return dbResult.getString(1);
return new SMTokens(dbResult.getString(1), dbResult.getString(2));
}
} catch (SQLException e) {
throw new DBCException("Error reading token info in database", e);
}
}
private RefreshTokenInfo findRefreshToken(String smAccessToken) throws DBException {
try (Connection dbCon = database.openConnection();
PreparedStatement dbStat = dbCon.prepareStatement(
"SELECT REFRESH_TOKEN_ID,SESSION_ID,USER_ID,REFRESH_TOKEN_EXPIRATION_TIME FROM CB_AUTH_TOKEN WHERE TOKEN_ID=?"
)
) {
dbStat.setString(1, smAccessToken);
try (var dbResult = dbStat.executeQuery()) {
if (!dbResult.next()) {
throw new SMException("Refresh token not found");
}
var refreshToken = dbResult.getString(1);
var sessionId = dbResult.getString(2);
var userId = dbResult.getString(3);
var expiredDate = dbResult.getTimestamp(4);
if (Timestamp.from(Instant.now()).after(expiredDate)) {
throw new SMRefreshTokenExpiredException("Refresh token expired");
}
return new RefreshTokenInfo(refreshToken, sessionId, userId);
}
} catch (SQLException e) {
throw new DBCException("Error reading token info in database", e);
@@ -1173,12 +1232,12 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
AuthAttemptSessionInfo authAttemptSessionInfo = readAuthAttemptSessionInfo(authId);
boolean isMainAuthSession = authAttemptSessionInfo.getSmSessionId() == null;
String token = null;
SMTokens smTokens = null;
SMAuthPermissions permissions = null;
if (!isMainAuthSession) {
//this is an additional authorization and we should to return the original permissions and userId
token = findTokenBySmSession(authAttemptSessionInfo.getSmSessionId());
permissions = getTokenPermissions(token);
smTokens = findTokenBySmSession(authAttemptSessionInfo.getSmSessionId());
permissions = getTokenPermissions(smTokens.getSmAccessToken());
}
String activeUserId = permissions == null ? null : permissions.getUserId();
@@ -1208,7 +1267,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
);
}
if (token == null && permissions == null) {
if (smTokens == null && permissions == null) {
try (Connection dbCon = database.openConnection()) {
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
String smSessionId;
@@ -1223,7 +1282,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
} else {
smSessionId = authAttemptSessionInfo.getSmSessionId();
}
token = generateAuthToken(smSessionId, activeUserId, dbCon);
smTokens = generateNewSessionToken(smSessionId, activeUserId, dbCon);
permissions = new SMAuthPermissions(activeUserId, smSessionId, getUserPermissions(activeUserId));
txn.commit();
}
@@ -1235,7 +1294,14 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
}
var authStatus = forceExpireAuthAfterSuccess ? SMAuthStatus.EXPIRED : SMAuthStatus.SUCCESS;
updateAuthStatus(authId, authStatus, storedUserData, null, permissions.getSessionId());
return SMAuthInfo.success(authId, token, permissions, authInfo.getAuthData());
return SMAuthInfo.success(
authId,
smTokens.getSmAccessToken(),
//refresh token must be sent only once
isMainAuthSession ? smTokens.getSmRefreshToken() : null,
permissions,
authInfo.getAuthData()
);
}
private AuthAttemptSessionInfo readAuthAttemptSessionInfo(@NotNull String authId) throws DBException {
@@ -1316,28 +1382,40 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
return userId;
}
protected String generateAuthToken(
protected SMTokens generateNewSessionToken(
@NotNull String smSessionId,
@Nullable String userId,
@NotNull Connection dbCon
) throws SQLException, DBException {
JDBCUtils.executeStatement(dbCon, "DELETE FROM CB_AUTH_TOKEN WHERE SESSION_ID=?", smSessionId);
try (PreparedStatement dbStat = dbCon.prepareStatement(
"INSERT INTO CB_AUTH_TOKEN(TOKEN_ID, SESSION_ID, USER_ID, EXPIRATION_TIME) " +
"VALUES(?,?,?,?)")) {
return generateNewSessionTokens(smSessionId, userId, dbCon);
}
String authToken = SecurityUtils.generatePassword(32);
dbStat.setString(1, authToken);
private SMTokens generateNewSessionTokens(@NotNull String smSessionId,
@Nullable String userId,
@NotNull Connection dbCon) throws SQLException {
try (PreparedStatement dbStat = dbCon.prepareStatement(
"INSERT INTO CB_AUTH_TOKEN(TOKEN_ID,SESSION_ID,USER_ID,EXPIRATION_TIME,REFRESH_TOKEN_ID,REFRESH_TOKEN_EXPIRATION_TIME) " +
"VALUES(?,?,?,?,?,?)")) {
String smAccessToken = SecurityUtils.generatePassword(32);
dbStat.setString(1, smAccessToken);
dbStat.setString(2, smSessionId);
if (userId == null) {
dbStat.setNull(3, Types.VARCHAR);
} else {
dbStat.setString(3, userId);
}
var expirationTime = Timestamp.valueOf(LocalDateTime.now().plusHours(TOKEN_HOURS_ALIVE_TIME));
dbStat.setTimestamp(4, expirationTime);
var accessTokenExpirationTime = Timestamp.valueOf(LocalDateTime.now().plusMinutes(ACCESS_TOKEN_TTL_IN_MINUTES));
dbStat.setTimestamp(4, accessTokenExpirationTime);
String smRefreshToken = SecurityUtils.generatePassword(32);
dbStat.setString(5, smRefreshToken);
var refreshTokenExpirationTime = Timestamp.valueOf(LocalDateTime.now().plusHours(REFRESH_TOKEN_TTL_IN_HOURS));
dbStat.setTimestamp(6, refreshTokenExpirationTime);
dbStat.execute();
return authToken;
return new SMTokens(smAccessToken, smRefreshToken);
}
}
@@ -1356,7 +1434,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
userId = dbResult.getString(1);
var expiredDate = dbResult.getTimestamp(2);
if (application.isMultiNode() && Timestamp.from(Instant.now()).after(expiredDate)) {
throw new SMException("Token expired");
throw new SMAccessTokenExpiredException("Token expired");
}
sessionId = dbResult.getString(3);
}
@@ -68,7 +68,7 @@ public class CBDatabase {
public static final String SCHEMA_UPDATE_SQL_PATH = "db/cb_schema_update_";
private static final int LEGACY_SCHEMA_VERSION = 1;
private static final int CURRENT_SCHEMA_VERSION = 8;
private static final int CURRENT_SCHEMA_VERSION = 9;
private static final String DEFAULT_DB_USER_NAME = "cb-data";
private static final String DEFAULT_DB_PWD_FILE = ".database-credentials.dat";
@@ -0,0 +1,52 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.security.internal;
import org.jkiss.code.NotNull;
public class RefreshTokenInfo {
@NotNull
private final String refreshToken;
@NotNull
private final String sessionId;
@NotNull
private final String userId;
public RefreshTokenInfo(@NotNull String refreshToken, @NotNull String sessionId, @NotNull String userId) {
this.refreshToken = refreshToken;
this.sessionId = sessionId;
this.userId = userId;
}
@NotNull
public String getRefreshToken() {
return refreshToken;
}
@NotNull
public String getSessionId() {
return sessionId;
}
@NotNull
public String getUserId() {
return userId;
}
}
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.ce.drivers.feature"
label="Cloudbeaver CE 3rd Party Drivers"
version="1.0.79.qualifier"
version="1.0.80.qualifier"
provider-name="DBeaver Corp">
<plugin id="io.cloudbeaver.resources.drivers.base" download-size="0" install-size="0" version="0.0.0"/>
@@ -9,6 +9,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.ce.drivers.feature</artifactId>
<version>1.0.79-SNAPSHOT</version>
<version>1.0.80-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
</project>
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.product.ce.feature"
label="%featureName"
version="22.1.5.qualifier"
version="22.2.0.qualifier"
provider-name="%providerName"
plugin="io.cloudbeaver.product.ce">
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.product.ce.feature</artifactId>
<version>22.1.5-SNAPSHOT</version>
<version>22.2.0-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
<build>
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.server.feature"
label="%featureName"
version="22.1.5.qualifier"
version="22.2.0.qualifier"
provider-name="%providerName"
plugin="io.cloudbeaver.server">
@@ -10,6 +10,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.server.feature</artifactId>
<version>22.1.5-SNAPSHOT</version>
<version>22.2.0-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
</project>
@@ -2,7 +2,7 @@
<feature
id="io.cloudbeaver.ws.feature"
label="%featureName"
version="1.0.9.qualifier"
version="1.0.10.qualifier"
provider-name="%providerName"
plugin="io.cloudbeaver.model">
@@ -10,6 +10,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.ws.feature</artifactId>
<version>1.0.9-SNAPSHOT</version>
<version>1.0.10-SNAPSHOT</version>
<packaging>eclipse-feature</packaging>
</project>
+1 -1
View File
@@ -19,7 +19,7 @@
<properties>
<dbeaver-product>CloudBeaver CE</dbeaver-product>
<dbeaver-version>22.1.5</dbeaver-version>
<dbeaver-version>22.2.0</dbeaver-version>
</properties>
<modules>
@@ -2,7 +2,7 @@
<?pde version="3.5"?>
<product name="CloudBeaver ce Server" uid="io.cloudbeaver.product" id="io.cloudbeaver.product.ce.product"
application="io.cloudbeaver.product.ce.application" version="22.1.5.qualifier" useFeatures="true"
application="io.cloudbeaver.product.ce.application" version="22.2.0.qualifier" useFeatures="true"
includeLaunchers="false">
<configIni use="default">
+1 -1
View File
@@ -9,7 +9,7 @@
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../</relativePath>
</parent>
<version>22.1.5-SNAPSHOT</version>
<version>22.2.0-SNAPSHOT</version>
<artifactId>web-server</artifactId>
<packaging>eclipse-repository</packaging>
<name>Cloudbeaver Server Product</name>
@@ -10,35 +10,6 @@ export default [
['app_shared_settingsMenu_theme', 'Theme'],
['app_shared_settingsMenu_lang', 'Language'],
['app_topnavbar_connection_schema_manager_not_selected', '-- not selected --'],
['app_navigationTree_openNodeTab', 'Open'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_limited', 'Elements are limited to {args.limit} items'],
['app_navigationTree_action_link_with_editor', 'Link with editor'],
['app_navigationTree_action_collapse_all', 'Collapse all'],
['app_navigationTree_settings_filter_title', 'Filter'],
['app_navigationTree_settings_filter_description', 'Show filtering field'],
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
['app_navigationTree_settings_state_title', 'Save tree state'],
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
['app_navigationTree_settings_folders_title', 'Folders'],
['app_navigationTree_settings_folders_description', 'Open any node as tree root node'],
['app_navigationTree_node_delete_confirmation', 'You\'re going to delete "{args.name}". Are you sure?'],
['app_navigationTree_node_delete_error', 'Failed to delete "{args.name}'],
['app_navigationTree_node_rename_error', 'Error occurred while renaming'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_node_not_found', 'Node not found'],
['app_navigationTree_connection_view', 'Connection view'],
['app_navigationTree_connection_view_option_simple', 'Simple'],
['app_navigationTree_connection_view_option_advanced', 'Advanced'],
['app_navigationTree_connection_view_option_custom', 'Custom'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'],
['app_navigationTree_refresh', 'Refresh'],
['app_navigationTree_refresh_error', 'Failed to refresh node'],
['app_navigationTree_node_change_name_error', 'Error occurred while changing name'],
['app_navigationTree_context_disconnect', 'Disconnect'],
['app_navigationTree_connection_group_user', 'User Connections'],
['app_navigationTree_connection_group_shared', 'Shared Connections'],
['app_shared_inlineEditor_dialog_title', 'Edit mode'],
['app_shared_inlineEditor_dialog_apply', 'Apply'],
['app_shared_inlineEditor_dialog_cancel', 'Cancel'],
@@ -10,34 +10,6 @@ export default [
['app_shared_settingsMenu_theme', 'Tema'],
['app_shared_settingsMenu_lang', 'Lingua'],
['app_topnavbar_connection_schema_manager_not_selected', '-- non selezionato --'],
['app_navigationTree_openNodeTab', 'Apri'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_limited', 'Elements are limited to {args.limit} items'],
['app_navigationTree_action_link_with_editor', 'Link with editor'],
['app_navigationTree_action_collapse_all', 'Collapse all'],
['app_navigationTree_settings_filter_title', 'Filter'],
['app_navigationTree_settings_filter_description', 'Show filtering field'],
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
['app_navigationTree_node_delete_confirmation', 'You\'re going to delete "{args.name}". Are you sure?'],
['app_navigationTree_node_delete_error', 'Failed to delete "{args.name}'],
['app_navigationTree_node_rename_error', 'Error occurred while renaming'],
['app_navigationTree_settings_state_title', 'Save tree state'],
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
['app_navigationTree_settings_folders_title', 'Folders'],
['app_navigationTree_settings_folders_description', 'Open any node as tree root node'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_node_not_found', 'Node not found'],
['app_navigationTree_connection_view', 'Vista di connessione'],
['app_navigationTree_connection_view_option_simple', 'Semplice'],
['app_navigationTree_connection_view_option_advanced', 'Avanzato'],
['app_navigationTree_connection_view_option_custom', 'Manuale'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Mostra oggetti di sistema'],
['app_navigationTree_refresh', 'Aggiorna'],
['app_navigationTree_refresh_error', 'Failed to refresh node'],
['app_navigationTree_context_disconnect', 'Scollega'],
['app_navigationTree_connection_group_user', 'Connessioni Utente'],
['app_navigationTree_connection_group_shared', 'Connessioni Condivise'],
['app_shared_inlineEditor_dialog_title', 'Modalità modifica'],
['app_shared_inlineEditor_dialog_apply', 'Applica'],
['app_shared_inlineEditor_dialog_cancel', 'Cancella'],
@@ -13,38 +13,6 @@ export default [
'app_shared_navigationTabsBar_placeholder',
'Нет открытых объектов для отображения. Дважды нажмите на элемент в дереве навигации, чтобы открыть его.',
],
['app_navigationTree_openNodeTab', 'Открыть'],
['app_navigationTree_search', 'Искать элементы...'],
['app_navigationTree_limited', 'Отображено {args.limit} элементов'],
['app_navigationTree_action_link_with_editor', 'Синхронизовать с редактором'],
['app_navigationTree_action_collapse_all', 'Свернуть все элементы'],
['app_navigationTree_settings_filter_title', 'Фильтр'],
['app_navigationTree_settings_filter_description', 'Показывать фильтр'],
['app_navigationTree_settings_filter_all_title', 'Фильтровать скрытые элементы'],
['app_navigationTree_settings_filter_all_description', 'Показывает скрытые элементы при использовании фильтра'],
['app_navigationTree_settings_state_title', 'Сохранять состояние дерева'],
['app_navigationTree_settings_state_description', 'Восстанавливает открытые узлы после обновления страницы'],
['app_navigationTree_settings_folders_title', 'Папки'],
['app_navigationTree_settings_folders_description', 'Позволяет открыть любой узел как начальный'],
['app_navigationTree_node_delete_confirmation', 'Вы собираетесь удалить "{args.name}". Вы уверены?'],
['app_navigationTree_node_delete_error', 'Ошибка удаления "{args.name}'],
['app_navigationTree_node_rename_error', 'Ошибка переименования ноды'],
['app_navigationTree_node_empty', 'Пусто'],
['app_navigationTree_node_not_found', 'Элемент не найден'],
['app_navigationTree_connection_view', 'Отображение'],
['app_navigationTree_connection_view_option_simple', 'Упрощенное'],
['app_navigationTree_connection_view_option_advanced', 'Продвинутое'],
['app_navigationTree_connection_view_option_custom', 'Настраиваемое'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'],
['app_navigationTree_refresh', 'Обновить'],
['app_navigationTree_refresh_error', 'Не удалось обновить узел'],
['app_navigationTree_node_change_name_error', 'Не удалось изменить название'],
['app_navigationTree_context_disconnect', 'Отключить'],
['app_navigationTree_connection_group_user', 'Пользовательские подключения'],
['app_navigationTree_connection_group_shared', 'Общие подключения'],
['app_shared_inlineEditor_dialog_title', 'Редактирование'],
['app_shared_inlineEditor_dialog_apply', 'Применить'],
['app_shared_inlineEditor_dialog_cancel', 'Отменить'],
@@ -10,35 +10,6 @@ export default [
['app_shared_settingsMenu_theme', '主题'],
['app_shared_settingsMenu_lang', '语言'],
['app_topnavbar_connection_schema_manager_not_selected', '-- 为选择 --'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_limited', 'Elements are limited to {args.limit} items'],
['app_navigationTree_action_link_with_editor', 'Link with editor'],
['app_navigationTree_action_collapse_all', 'Collapse all'],
['app_navigationTree_settings_filter_title', 'Filter'],
['app_navigationTree_settings_filter_description', 'Show filtering field'],
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
['app_navigationTree_settings_state_title', 'Save tree state'],
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
['app_navigationTree_settings_folders_title', 'Folders'],
['app_navigationTree_settings_folders_description', 'Open any node as tree root node'],
['app_navigationTree_node_delete_confirmation', 'You\'re going to delete "{args.name}". Are you sure?'],
['app_navigationTree_node_delete_error', 'Failed to delete "{args.name}'],
['app_navigationTree_node_rename_error', 'Error occurred while renaming'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_openNodeTab', '打开'],
['app_navigationTree_connection_view', '连接视图'],
['app_navigationTree_node_not_found', 'Node not found'],
['app_navigationTree_connection_view_option_simple', '简单'],
['app_navigationTree_connection_view_option_advanced', '完善'],
['app_navigationTree_connection_view_option_custom', '自定义'],
['app_navigationTree_connection_view_option_showSystemObjects', '展示系统对象'],
['app_navigationTree_refresh', '刷新'],
['app_navigationTree_refresh_error', 'Failed to refresh node'],
['app_navigationTree_node_change_name_error', '更改名称时发生错误'],
['app_navigationTree_context_disconnect', '断开连接'],
['app_navigationTree_connection_group_user', '用户连接'],
['app_navigationTree_connection_group_shared', '共享连接'],
['app_shared_inlineEditor_dialog_title', '编辑模式'],
['app_shared_inlineEditor_dialog_apply', '应用'],
['app_shared_inlineEditor_dialog_cancel', '取消'],
@@ -119,7 +119,7 @@ UserInfoIncludes
return authInfo as AuthInfo;
}
finishFederatedAuthentication(authId: string, link?: boolean): ITask<UserInfo | null> {
finishFederatedAuthentication(authId: string, linkUser?: boolean): ITask<UserInfo | null> {
let activeTask: ITask<AuthInfo> | undefined;
return new AutoRunningTask<UserInfo | null>(() => this.performUpdate(
@@ -139,7 +139,7 @@ UserInfoIncludes
async () => {
const { authInfo } = await this.graphQLService.sdk.getAuthStatus({
authId,
linkUser: link,
linkUser,
customIncludeOriginDetails: true,
});
return authInfo as AuthInfo;
@@ -7,8 +7,6 @@
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="preload" as="font"
crossorigin="anonymous" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" crossorigin="anonymous" />
<link rel="prefetch" href="icons/icons.svg" as="image" type="image/svg+xml" />
<script type="text/javascript">
@@ -181,23 +181,33 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
const nodePaths = isResourceKeyList(key) ? key.list : [key];
await this.performUpdate(key, [], async () => {
await this.graphQLService.sdk.navDeleteNodes({ nodePaths });
runInAction(() => {
const parents: string[] = [];
const deletedIds: string[][] = [];
const deletedPaths: string[] = [];
try {
for (const path of nodePaths) {
const node = this.navNodeInfoResource.get(path);
if (node) {
parents.push(node.parentId);
deletedIds.push([path]);
}
await this.graphQLService.sdk.navDeleteNodes({ nodePaths: [path] });
deletedPaths.push(path);
}
} finally {
runInAction(() => {
const deletionMap = new Map<string, string[]>();
this.deleteInNode(resourceKeyList(parents), deletedIds);
});
for (const path of deletedPaths) {
const node = this.navNodeInfoResource.get(path);
if (node) {
const deletedIds = deletionMap.get(node.parentId) ?? [];
deletedIds.push(path);
deletionMap.set(node.parentId, deletedIds);
}
}
const keys = resourceKeyList([...deletionMap.keys()]);
const nodes = [...deletionMap.values()];
this.deleteInNode(keys, nodes);
});
}
});
}
@@ -66,8 +66,8 @@ export class AuthenticationService extends Bootstrap {
this.configureIdentityProvider = action;
}
async authUser(providerId: string | null = null, link?: boolean): Promise<void> {
await this.auth(false, { providerId, link });
async authUser(providerId: string | null = null, linkUser?: boolean): Promise<void> {
await this.auth(false, { providerId, linkUser });
}
async logout(): Promise<void> {
@@ -170,7 +170,7 @@ export class AuthenticationService extends Bootstrap {
return;
}
await this.auth(true, { accessRequest: true, providerId: null, link: true });
await this.auth(true, { accessRequest: true, providerId: null, linkUser: false });
}
register(): void {
@@ -188,7 +188,7 @@ export class AuthenticationService extends Bootstrap {
return;
}
await this.auth(false, { providerId: null, link: true, accessRequest: true });
await this.auth(false, { providerId: null, linkUser: false, accessRequest: true });
});
this.authProviderService.requestAuthProvider.addHandler(this.requestAuthProviderHandler);
}
@@ -202,7 +202,7 @@ export class AuthenticationService extends Bootstrap {
const action = contexts.getContext(sessionActionContext);
if (isAutoLoginSessionAction(data)) {
const user = await this.userInfoResource.finishFederatedAuthentication(data['auth-id'], true);
const user = await this.userInfoResource.finishFederatedAuthentication(data['auth-id'], false);
if (user && this.authPromise) {
this.authDialogService.closeLoginForm(this.authPromise);
@@ -67,7 +67,7 @@ export const AuthDialog: DialogComponent<IAuthOptions, null> = observer(function
payload: {
providerId,
configurationId,
link = false,
linkUser = false,
accessRequest = false,
},
options,
@@ -111,7 +111,7 @@ export const AuthDialog: DialogComponent<IAuthOptions, null> = observer(function
}
async function login() {
await dialogData.login(link);
await dialogData.login(linkUser);
rejectDialog();
}
@@ -29,7 +29,7 @@ interface IData {
providers: AuthProvider[];
configurations: AuthProvider[];
login: (link: boolean) => Promise<void>;
login: (linkUser: boolean) => Promise<void>;
}
interface IState {
@@ -139,7 +139,7 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string |
}
return false;
},
async login(link: boolean): Promise<void> {
async login(linkUser: boolean): Promise<void> {
if (!state.activeProvider || this.authenticating) {
return;
}
@@ -148,7 +148,7 @@ export function useAuthDialogState(accessRequest: boolean, providerId: string |
try {
await authInfoService.login(state.activeProvider.id, {
credentials: state.credentials,
linkUser: link,
linkUser,
});
} catch (exception: any) {
if (this.destroyed) {
@@ -9,6 +9,6 @@
export interface IAuthOptions {
providerId: string | null;
configurationId?: string;
link?: boolean;
linkUser?: boolean;
accessRequest?: boolean;
}
@@ -40,7 +40,7 @@ export class PluginBootstrap extends Bootstrap {
label: 'authentication_login',
tooltip: 'authentication_login',
},
{ onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, link: true }) }
{ onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, linkUser: false }) }
),
];
}
@@ -189,7 +189,8 @@ export class NavigationTabsService extends View<ITab> {
}
}
this.callHandlerCallback(tab, handler => handler.onClose);
await this.callHandlerCallback(tab, handler => handler.onClose);
await this.callHandlerCallback(tab, handler => handler.onUnload);
}
this.closeTabSilent(tabId, skipHandlers);
@@ -288,7 +289,7 @@ export class NavigationTabsService extends View<ITab> {
}
}
unloadTabs(): void {
async unloadTabs(): Promise<void> {
// if (this.administrationScreenService.publicDisabled) {
// return;
// }
@@ -296,7 +297,7 @@ export class NavigationTabsService extends View<ITab> {
for (const tab of this.tabsMap.values()) {
if (tab.userId !== this.userInfoResource.getId()) {
if (tab.restored) {
this.callHandlerCallback(tab, handler => handler.onClose);
await this.callHandlerCallback(tab, handler => handler.onUnload);
tab.restored = false;
}
}
@@ -313,10 +314,6 @@ export class NavigationTabsService extends View<ITab> {
return;
}
for (const handler of this.handlers.values()) {
await handler.onNavInit?.();
}
const removedTabs: string[] = [];
for (const tabId of this.userTabsState.tabs) {
@@ -347,7 +344,7 @@ export class NavigationTabsService extends View<ITab> {
});
}
private callHandlerCallback(tab: ITab, selector: (handler: TabHandler) => TabHandlerEvent | undefined) {
private async callHandlerCallback(tab: ITab, selector: (handler: TabHandler) => TabHandlerEvent | undefined) {
const handler = this.handlers.get(tab.handlerId);
if (!handler) {
@@ -357,7 +354,7 @@ export class NavigationTabsService extends View<ITab> {
const callback = selector(handler);
if (callback) {
callback.call(handler, tab);
await callback.call(handler, tab);
}
}
@@ -26,9 +26,8 @@ export interface TabHandlerPanelProps<T = any> {
}
export type TabHandlerPanelComponent<T = any> = React.FunctionComponent<TabHandlerPanelProps<T>>;
export type TabHandlerNavInitEvent = () => Promise<void> | void;
export type TabHandlerCloseEvent<T = any> = (tab: ITab<T>) => Promise<boolean> | boolean;
export type TabHandlerEvent<T = any> = (tab: ITab<T>) => void;
export type TabHandlerEvent<T = any> = (tab: ITab<T>) => Promise<void> | void;
export type TabRestoreEvent<T = any> = (tab: ITab<T>) => Promise<boolean> | boolean;
export interface TabHandlerOptions<TState = any> {
@@ -39,8 +38,7 @@ export interface TabHandlerOptions<TState = any> {
canClose?: TabHandlerCloseEvent<TState>;
onClose?: TabHandlerEvent<TState>;
onRestore?: TabRestoreEvent<TState>;
onNavInit?: TabHandlerNavInitEvent;
onNavUnload?: TabHandlerNavInitEvent;
onUnload?: TabHandlerEvent<TState>;
extensions?: Array<IExtension<ITab<TState>>>;
}
@@ -52,8 +50,7 @@ export class TabHandler<TState = any> {
onClose?: TabHandlerEvent<TState>;
canClose?: TabHandlerCloseEvent<TState>;
onRestore?: TabRestoreEvent<TState>;
onNavInit?: TabHandlerNavInitEvent;
onNavUnload?: TabHandlerNavInitEvent;
onUnload?: TabHandlerEvent<TState>;
extensions?: Array<IExtension<ITab<TState>>>;
constructor(options: TabHandlerOptions<TState>) {
@@ -64,8 +61,7 @@ export class TabHandler<TState = any> {
this.canClose = options.canClose;
this.onClose = options.onClose;
this.onRestore = options.onRestore;
this.onNavInit = options.onNavInit;
this.onNavUnload = options.onNavUnload;
this.onUnload = options.onUnload;
this.extensions = options.extensions;
}
}
@@ -0,0 +1,36 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { LocalizationService } from '@cloudbeaver/core-localization';
@injectable()
export class LocaleService extends Bootstrap {
constructor(private readonly localizationService: LocalizationService) {
super();
}
register(): void | Promise<void> {
this.localizationService.addProvider(this.provider.bind(this));
}
load(): void | Promise<void> { }
private async provider(locale: string) {
switch (locale) {
case 'ru':
return (await import('./locales/ru')).default;
case 'it':
return (await import('./locales/it')).default;
case 'zh':
return (await import('./locales/zh')).default;
default:
return (await import('./locales/en')).default;
}
}
}
@@ -10,9 +10,10 @@ import { observer } from 'mobx-react-lite';
import { useMemo, useCallback, useEffect } from 'react';
import styled, { css, use } from 'reshadow';
import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag, FolderExplorer, FolderExplorerPath, Loader, useFolderExplorer, useMapResource, useObjectRef } from '@cloudbeaver/core-blocks';
import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag, FolderExplorer, FolderExplorerPath, Loader, TreeNodeNested, TreeNodeNestedMessage, TREE_NODE_STYLES, useFolderExplorer, useMapResource, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { Translate } from '@cloudbeaver/core-localization';
import { type NavNode, ROOT_NODE_PATH, NavTreeResource, NavNodeInfoResource, EObjectFeature } from '@cloudbeaver/core-navigation-tree';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
@@ -281,7 +282,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
const hasChildren = (children.data?.length || 0) > 0;
const loaderAvailable = !foldersTree || context.folderExplorer.root === root;
return styled(useStyles(styles, style))(
return styled(useStyles(TREE_NODE_STYLES, styles, style))(
<>
<ElementsTreeTools tree={tree} style={style} />
<tree-box>
@@ -305,11 +306,9 @@ export const ElementsTree = observer<Props>(function ElementsTree({
bottom: dropOutside.bottom,
})}
>
<NavigationNodeNested
component={NavigationNodeElement}
path={folderExplorer.state.path}
root
/>
<TreeNodeNested root>
<TreeNodeNestedMessage><Translate token='app_navigationTree_drop_here' /></TreeNodeNestedMessage>
</TreeNodeNested>
</drop-outside>
<NavigationNodeNested
ref={dropOutside.nestedRef}
@@ -12,6 +12,7 @@ import styled, { css } from 'reshadow';
import { useUserData } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { Translate } from '@cloudbeaver/core-localization';
import { NavNodeInfoResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import { usePermission, EPermission } from '@cloudbeaver/core-root';
import { CaptureView } from '@cloudbeaver/core-view';
@@ -110,8 +111,7 @@ export const NavigationTree = observer(function NavigationTree() {
emptyPlaceholder={() => styled(navigationTreeStyles)(
<center>
<message>
No connections.<br />
Use the top menu to setup connection to your database.
<Translate token='app_navigationTree_empty_placeholder' />
</message>
</center>
)}
@@ -58,7 +58,7 @@ export class NavNodeContextMenuService extends Bootstrap {
.mapArray(data, nodeId => this.navNodeInfoResource.get(nodeId))
.filter<NavNode>(Boolean as any)
.map(node => node.name)
.join();
.join(', ');
const result = await this.commonDialogService.open(ConfirmationDialogDelete, {
title: 'ui_data_delete_confirmation',
@@ -8,8 +8,12 @@
import '@testing-library/jest-dom';
import { mockAuthentication } from '@cloudbeaver/core-authentication/mocks/mockAuthentication';
import { createApp } from '@cloudbeaver/core-cli/tests/utils/createApp';
import { getService } from '@cloudbeaver/core-cli/tests/utils/getService';
import { createGQLEndpoint } from '@cloudbeaver/core-root/mocks/createGQLEndpoint';
import { mockAppInit } from '@cloudbeaver/core-root/mocks/mockAppInit';
import { mockGraphQL } from '@cloudbeaver/core-root/mocks/mockGraphQL';
import datasourceContextSwitchPlugin from '@cloudbeaver/plugin-datasource-context-switch';
import navigationTabsPlugin from '@cloudbeaver/plugin-navigation-tabs';
import topAppBarPlugin from '@cloudbeaver/plugin-top-app-bar';
@@ -17,6 +21,7 @@ import topAppBarPlugin from '@cloudbeaver/plugin-top-app-bar';
import { navigationTreePlugin } from '../../manifest';
import { NavNodeViewService } from './NavNodeViewService';
const endpoint = createGQLEndpoint();
const app = createApp(
topAppBarPlugin,
navigationTabsPlugin,
@@ -24,6 +29,11 @@ const app = createApp(
navigationTreePlugin
);
mockGraphQL(
...mockAppInit(endpoint),
...mockAuthentication(endpoint)
);
beforeAll(() => app.init());
describe('filterDuplicates', () => {
@@ -0,0 +1,33 @@
export default [
['app_navigationTree_openNodeTab', 'Open'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_limited', 'Elements are limited to {args.limit} items'],
['app_navigationTree_action_link_with_editor', 'Link with editor'],
['app_navigationTree_action_collapse_all', 'Collapse all'],
['app_navigationTree_settings_filter_title', 'Filter'],
['app_navigationTree_settings_filter_description', 'Show filtering field'],
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
['app_navigationTree_settings_state_title', 'Save tree state'],
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
['app_navigationTree_settings_folders_title', 'Folders'],
['app_navigationTree_settings_folders_description', 'Open any node as tree root node'],
['app_navigationTree_node_delete_confirmation', 'You\'re going to delete "{args.name}". Are you sure?'],
['app_navigationTree_node_delete_error', 'Failed to delete "{args.name}'],
['app_navigationTree_node_rename_error', 'Error occurred while renaming'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_node_not_found', 'Node not found'],
['app_navigationTree_connection_view', 'Connection view'],
['app_navigationTree_connection_view_option_simple', 'Simple'],
['app_navigationTree_connection_view_option_advanced', 'Advanced'],
['app_navigationTree_connection_view_option_custom', 'Custom'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'],
['app_navigationTree_refresh', 'Refresh'],
['app_navigationTree_refresh_error', 'Failed to refresh node'],
['app_navigationTree_node_change_name_error', 'Error occurred while changing name'],
['app_navigationTree_context_disconnect', 'Disconnect'],
['app_navigationTree_connection_group_user', 'User Connections'],
['app_navigationTree_connection_group_shared', 'Shared Connections'],
['app_navigationTree_empty_placeholder', 'No Connections.\nUse the top menu to setup connection to your database.'],
['app_navigationTree_drop_here', 'Drop here...'],
];
@@ -0,0 +1,31 @@
export default [
['app_navigationTree_openNodeTab', 'Apri'],
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_limited', 'Elements are limited to {args.limit} items'],
['app_navigationTree_action_link_with_editor', 'Link with editor'],
['app_navigationTree_action_collapse_all', 'Collapse all'],
['app_navigationTree_settings_filter_title', 'Filter'],
['app_navigationTree_settings_filter_description', 'Show filtering field'],
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
['app_navigationTree_node_delete_confirmation', 'You\'re going to delete "{args.name}". Are you sure?'],
['app_navigationTree_node_delete_error', 'Failed to delete "{args.name}'],
['app_navigationTree_node_rename_error', 'Error occurred while renaming'],
['app_navigationTree_settings_state_title', 'Save tree state'],
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
['app_navigationTree_settings_folders_title', 'Folders'],
['app_navigationTree_settings_folders_description', 'Open any node as tree root node'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_node_not_found', 'Node not found'],
['app_navigationTree_connection_view', 'Vista di connessione'],
['app_navigationTree_connection_view_option_simple', 'Semplice'],
['app_navigationTree_connection_view_option_advanced', 'Avanzato'],
['app_navigationTree_connection_view_option_custom', 'Manuale'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Mostra oggetti di sistema'],
['app_navigationTree_refresh', 'Aggiorna'],
['app_navigationTree_refresh_error', 'Failed to refresh node'],
['app_navigationTree_context_disconnect', 'Scollega'],
['app_navigationTree_connection_group_user', 'Connessioni Utente'],
['app_navigationTree_empty_placeholder', 'Nessuna Connessione.\nUsa il menù in altro per impostare una nuova connessione.'],
['app_navigationTree_drop_here', 'Drop here...'],
];
@@ -0,0 +1,36 @@
export default [
['app_navigationTree_openNodeTab', 'Открыть'],
['app_navigationTree_search', 'Искать элементы...'],
['app_navigationTree_limited', 'Отображено {args.limit} элементов'],
['app_navigationTree_action_link_with_editor', 'Синхронизовать с редактором'],
['app_navigationTree_action_collapse_all', 'Свернуть все элементы'],
['app_navigationTree_settings_filter_title', 'Фильтр'],
['app_navigationTree_settings_filter_description', 'Показывать фильтр'],
['app_navigationTree_settings_filter_all_title', 'Фильтровать скрытые элементы'],
['app_navigationTree_settings_filter_all_description', 'Показывает скрытые элементы при использовании фильтра'],
['app_navigationTree_settings_state_title', 'Сохранять состояние дерева'],
['app_navigationTree_settings_state_description', 'Восстанавливает открытые узлы после обновления страницы'],
['app_navigationTree_settings_folders_title', 'Папки'],
['app_navigationTree_settings_folders_description', 'Позволяет открыть любой узел как начальный'],
['app_navigationTree_node_delete_confirmation', 'Вы собираетесь удалить "{args.name}". Вы уверены?'],
['app_navigationTree_node_delete_error', 'Ошибка удаления "{args.name}'],
['app_navigationTree_node_rename_error', 'Ошибка переименования ноды'],
['app_navigationTree_node_empty', 'Пусто'],
['app_navigationTree_node_not_found', 'Элемент не найден'],
['app_navigationTree_connection_view', 'Отображение'],
['app_navigationTree_connection_view_option_simple', 'Упрощенное'],
['app_navigationTree_connection_view_option_advanced', 'Продвинутое'],
['app_navigationTree_connection_view_option_custom', 'Настраиваемое'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'],
['app_navigationTree_refresh', 'Обновить'],
['app_navigationTree_refresh_error', 'Не удалось обновить узел'],
['app_navigationTree_node_change_name_error', 'Не удалось изменить название'],
['app_navigationTree_context_disconnect', 'Отключить'],
['app_navigationTree_connection_group_user', 'Пользовательские подключения'],
['app_navigationTree_connection_group_shared', 'Общие подключения'],
['app_navigationTree_empty_placeholder', 'Нет подключений.\nИспользуйте верхнее меню для создания подключения к базе.'],
['app_navigationTree_drop_here', 'Drop here...'],
];
@@ -0,0 +1,33 @@
export default [
['app_navigationTree_search', 'Search elements...'],
['app_navigationTree_limited', 'Elements are limited to {args.limit} items'],
['app_navigationTree_action_link_with_editor', 'Link with editor'],
['app_navigationTree_action_collapse_all', 'Collapse all'],
['app_navigationTree_settings_filter_title', 'Filter'],
['app_navigationTree_settings_filter_description', 'Show filtering field'],
['app_navigationTree_settings_filter_all_title', 'Show collapsed'],
['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'],
['app_navigationTree_settings_state_title', 'Save tree state'],
['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'],
['app_navigationTree_settings_folders_title', 'Folders'],
['app_navigationTree_settings_folders_description', 'Open any node as tree root node'],
['app_navigationTree_node_delete_confirmation', 'You\'re going to delete "{args.name}". Are you sure?'],
['app_navigationTree_node_delete_error', 'Failed to delete "{args.name}'],
['app_navigationTree_node_rename_error', 'Error occurred while renaming'],
['app_navigationTree_node_empty', 'Empty'],
['app_navigationTree_openNodeTab', '打开'],
['app_navigationTree_connection_view', '连接视图'],
['app_navigationTree_node_not_found', 'Node not found'],
['app_navigationTree_connection_view_option_simple', '简单'],
['app_navigationTree_connection_view_option_advanced', '完善'],
['app_navigationTree_connection_view_option_custom', '自定义'],
['app_navigationTree_connection_view_option_showSystemObjects', '展示系统对象'],
['app_navigationTree_refresh', '刷新'],
['app_navigationTree_refresh_error', 'Failed to refresh node'],
['app_navigationTree_node_change_name_error', '更改名称时发生错误'],
['app_navigationTree_context_disconnect', '断开连接'],
['app_navigationTree_connection_group_user', '用户连接'],
['app_navigationTree_connection_group_shared', '共享连接'],
['app_navigationTree_empty_placeholder', 'No Connections.\nUse the top menu to setup connection to your database.'],
['app_navigationTree_drop_here', 'Drop here...'],
];
@@ -8,6 +8,7 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { LocaleService } from './LocaleService';
import { ElementsTreeToolsMenuService } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService';
import { NavigationTreeBootstrap } from './NavigationTree/NavigationTreeBootstrap';
import { NavigationTreeService } from './NavigationTree/NavigationTreeService';
@@ -18,6 +19,7 @@ import { NavNodeViewService } from './NodesManager/NavNodeView/NavNodeViewServic
export const navigationTreePlugin: PluginManifest = {
info: { name: 'Navigation Tree plugin' },
providers: [
LocaleService,
NavigationTreeService,
ElementsTreeToolsMenuService,
NavigationTreeBootstrap,
@@ -62,32 +62,12 @@ export class ObjectPropertyTableFooterService {
onClick: async context => {
const nodes = this.getSelectedNodes(context.data.tableState.selectedList)
.filter(node => node.features?.includes(ENodeFeature.canDelete));
const nodeNames = nodes.map(getNodeDisplayName);
const result = await this.commonDialogService.open(ConfirmationDialogDelete, {
title: 'ui_data_delete_confirmation',
message: `You're going to delete following items: "${nodeNames.join(', ')}". Are you sure?`,
confirmActionText: 'ui_delete',
});
if (result === DialogueStateResult.Rejected) {
return;
}
const deleted: string[] = [];
try {
for (const node of nodes) {
await this.navTreeResource.deleteNode(node.id);
deleted.push(node.id);
}
await this.navTreeResource.deleteNode(resourceKeyList(nodes.map(node => node.id)));
} catch (exception: any) {
this.notificationService.logException(exception, 'Failed to delete item');
}
if (deleted.length) {
context.data.tableState.unselect(deleted);
}
},
});
}
@@ -28,6 +28,9 @@ const menuStyles = css`
cursor: pointer;
width: 100%;
}
menu-container:not([|menuEmpty]) value {
padding-right: 8px;
}
menu-box {
display: flex;
height: 100%;
@@ -183,6 +183,11 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
try {
this.exception = null;
this.nodeInfo.nodeId = await this.actions.rename(this, this.nodeInfo.nodeId, name);
if (this.nodeInfo.parents.length > 0) {
this.nodeInfo.parents.splice(this.nodeInfo.parents.length - 1, 1, this.nodeInfo.nodeId);
}
this.markOutdated();
this.saved = true;
this.loaded = false;
@@ -65,13 +65,13 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
if (
!['undefined', 'object'].includes(typeof value.nodeInfo)
|| !['string', 'undefined'].includes(typeof value.name)
|| !['string', 'undefined', 'object'].includes(typeof value.nodeInfo?.nodeId)
|| !['string', 'undefined'].includes(typeof value.nodeInfo?.nodeId)
|| !['undefined', 'object'].includes(typeof value.nodeInfo?.parents)
|| !['undefined', 'object'].includes(typeof value.executionContext)
|| !['string', 'undefined', 'object'].includes(typeof value.executionContext?.connectionId)
|| !['string', 'undefined', 'object'].includes(typeof value.executionContext?.id)
|| !['string', 'undefined', 'object'].includes(typeof value.executionContext?.defaultCatalog)
|| !['string', 'undefined', 'object'].includes(typeof value.executionContext?.defaultSchema)
|| !['string', 'undefined'].includes(typeof value.executionContext?.connectionId)
|| !['string', 'undefined'].includes(typeof value.executionContext?.id)
|| !['string', 'undefined'].includes(typeof value.executionContext?.defaultCatalog)
|| !['string', 'undefined'].includes(typeof value.executionContext?.defaultSchema)
) {
map.delete(key);
}
@@ -119,6 +119,11 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
return dataSource;
},
onDestroy: (_, editorId) => this.deleteState(editorId),
onUnload: async dataSource => {
if (dataSource instanceof ResourceSqlDataSource) {
await dataSource.write();
}
},
canDestroy: async (dataSource, editorId) => {
try {
if (dataSource instanceof ResourceSqlDataSource) {
@@ -70,6 +70,7 @@ export class SqlEditorTabService extends Bootstrap {
getTabComponent: () => SqlEditorTab,
getPanelComponent: () => SqlEditorPanel,
onRestore: this.handleTabRestore.bind(this),
onUnload: this.handleTabUnload.bind(this),
onClose: this.handleTabClose.bind(this),
canClose: this.handleCanTabClose.bind(this),
extensions: [
@@ -270,9 +271,9 @@ export class SqlEditorTabService extends Bootstrap {
typeof tab.handlerState.editorId !== 'string'
|| typeof tab.handlerState.editorId !== 'string'
|| typeof tab.handlerState.order !== 'number'
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.currentTabId)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.source)
|| !['string', 'undefined', 'object'].includes(typeof tab.handlerState.currentModeId)
|| !['string', 'undefined'].includes(typeof tab.handlerState.currentTabId)
|| !['string', 'undefined'].includes(typeof tab.handlerState.source)
|| !['string', 'undefined'].includes(typeof tab.handlerState.currentModeId)
|| !Array.isArray(tab.handlerState.modeState)
|| !Array.isArray(tab.handlerState.tabs)
|| !Array.isArray(tab.handlerState.executionPlanTabs)
@@ -448,17 +449,21 @@ export class SqlEditorTabService extends Bootstrap {
return canDestroyDatasource;
}
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
private async handleTabUnload(editorTab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.executionContext) {
await this.sqlEditorService.destroyContext(dataSource.executionContext);
}
await this.sqlDataSourceService.destroy(editorTab.handlerState.editorId);
await this.sqlDataSourceService.unload(editorTab.handlerState.editorId);
this.sqlResultTabsService.removeResultTabs(editorTab.handlerState);
}
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
await this.sqlDataSourceService.destroy(editorTab.handlerState.editorId);
}
}
function findMinimalFree(array: number[], base: number): number {
@@ -31,6 +31,7 @@ interface ISqlDataSourceFabric {
key: string;
getDataSource: ISqlDataSourceFactory;
onDestroy?: (dataSource: ISqlDataSource, editorId: string) => Promise<void> | void;
onUnload?: (dataSource: ISqlDataSource, editorId: string) => Promise<void> | void;
canDestroy?: (dataSource: ISqlDataSource, editorId: string) => Promise<boolean> | boolean;
}
@@ -146,6 +147,14 @@ export class SqlDataSourceService {
if (activeProvider) {
await this.destroyProvider(editorId, activeProvider);
}
}
async unload(editorId: string): Promise<void> {
const activeProvider = this.providers.get(editorId);
if (activeProvider) {
await this.unloadProvider(editorId, activeProvider);
}
this.providers.delete(editorId);
}
@@ -158,8 +167,12 @@ export class SqlDataSourceService {
this.dataSourceProviders.set(dataSourceOptions.key, dataSourceOptions);
}
private async destroyProvider(editorId: string, provider: ISqlDataSourceProvider): Promise<void> {
await provider.provider.onDestroy?.(provider.dataSource, editorId);
private async unloadProvider(editorId: string, provider: ISqlDataSourceProvider): Promise<void> {
await provider.provider.onUnload?.(provider.dataSource, editorId);
await provider.dataSource.dispose();
}
private async destroyProvider(editorId: string, provider: ISqlDataSourceProvider): Promise<void> {
await provider.provider.onDestroy?.(provider.dataSource, editorId);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cloudbeaver/product-default",
"sideEffects": false,
"version": "22.1.5",
"version": "22.2.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",