CB-2888 project sync (#1508)

* CB-3115 Add new create/delete project events

Implements new events

* CB-3115 Add new create/delete project events

Renames WSProjectLifeCycleEvent to WSProjectLifeCycleEvent
Adds BaseWebSession#addSessionEvent call in WsProjectLifeCycleEventHandler

* CB-3127 events for project permissions

* CB-2888 TE: Add synchronization for projects

Renames WSProjectUpdatedEventHandler to WSAbstractProjectEventHandler and
 WsProjectLifeCycleEventHandler to WSProjectUpdatedEventHandler

* CB-3127 code style fix

* CB-3127 rename parameters

* CB-3114 support project remove/add event

* CB-3127 event filter fix

* CB-2888 TE: Add synchronization for projects

Moves validateEvent call to WSAbstractProjectEventHandler;
Adds the use of an implementation class logger to the WSAbstractProjectEventHandler.

* CB-2888 TE: Add synchronization for projects

Fixes missed copyright

* CB-3127 filter event fix

* CB-2888 fix type naming

* CB-3127 project update event add remove connections from session

* CB-2888 TE: Add synchronization for projects

Adds dispatch of project creation/deletion events to WebServiceRM

* CB-3127 load connections when creating web project

* CB-3114 update active projects on active project remove

* CB-3114 update active projects via handler on project delete

* CB-2888 mark root tree as outdated on project add event

* CB-2888 move tree outdate logic to the useElementsTree hook

* CB-2888 fix conflict with devel

---------

Co-authored-by: Iaroslav Emelianov <iaroslav.emelianov@dbeaver.com>
Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: Alexander Skoblikov <aleksandr.skoblikov@dbeaver.com>
This commit is contained in:
Ainur
2023-03-23 16:42:08 +03:00
committed by GitHub
co-authored by Iaroslav Emelianov naumov dariamarutkina Alexander Skoblikov
parent ba885fb7eb
commit 837d321b3f
18 changed files with 459 additions and 127 deletions
@@ -190,7 +190,7 @@ public class LocalResourceController implements RMController {
.collect(Collectors.toList());
}
private Set<RMProjectPermission> getProjectPermissions(@Nullable String projectId, RMProjectType projectType) throws DBException {
private Set<RMProjectPermission> getProjectPermissions(@Nullable String projectId, @NotNull RMProjectType projectType) throws DBException {
var activeUserCreds = credentialsProvider.getActiveUserCredentials();
switch (projectType) {
@@ -19,6 +19,7 @@ package io.cloudbeaver.model.session;
import io.cloudbeaver.model.app.WebApplication;
import io.cloudbeaver.websocket.CBWebSessionEventHandler;
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.auth.SMAuthInfo;
@@ -168,4 +169,12 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
public boolean isProjectAccessible(String projectId) {
return userContext.getAccessibleProjectIds().contains(projectId);
}
public void addSessionProject(@NotNull String projectId) throws DBException {
userContext.getAccessibleProjectIds().add(projectId);
}
public void removeSessionProject(@Nullable String projectId) throws DBException {
userContext.getAccessibleProjectIds().remove(projectId);
}
}
@@ -302,6 +302,7 @@ public class WebSession extends BaseWebSession
this.navigatorModel.dispose();
this.navigatorModel = null;
}
this.connections.clear();
loadProjects();
@@ -310,13 +311,6 @@ public class WebSession extends BaseWebSession
this.navigatorModel.initialize();
this.locale = Locale.getDefault().getLanguage();
try {
this.refreshConnections();
} catch (Exception e) {
addSessionError(e);
log.error("Error getting connection list", e);
}
}
private void loadProjects() {
@@ -360,35 +354,17 @@ public class WebSession extends BaseWebSession
if (!project.isShared() || application.isConfigurationMode()) {
getWorkspace().setActiveProject(sessionProject);
}
for (DBPDataSourceContainer ds : dataSourceRegistry.getDataSources()) {
addConnection(new WebConnectionInfo(this, ds));
}
Throwable lastError = dataSourceRegistry.getLastError();
if (lastError != null) {
addSessionError(lastError);
log.error("Error refreshing connections from project '" + project.getId() + "'", lastError);
}
return sessionProject;
}
public void refreshConnections() {
// Add all provided datasources to the session
List<WebConnectionInfo> connList = new ArrayList<>();
for (DBPProject project : getWorkspace().getProjects()) {
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
for (DBPDataSourceContainer ds : registry.getDataSources()) {
connList.add(new WebConnectionInfo(this, ds));
}
Throwable lastError = registry.getLastError();
if (lastError != null) {
addSessionError(lastError);
log.error("Error refreshing connections from project '" + project.getId() + "'", lastError);
}
}
// Add all provided datasources to the session
synchronized (connections) {
connections.clear();
for (WebConnectionInfo connectionInfo : connList) {
connections.put(connectionInfo.getId(), connectionInfo);
}
}
}
public void filterAccessibleConnections(List<WebConnectionInfo> connections) {
connections.removeIf(c -> !isDataSourceAccessible(c.getDataSourceContainer()));
}
@@ -980,20 +956,41 @@ public class WebSession extends BaseWebSession
return getWorkspace().getProjects();
}
public void addSessionProject(WebProjectImpl project) {
public void addSessionProject(@NotNull WebProjectImpl project) {
getWorkspace().addProject(project);
if (navigatorModel != null) {
navigatorModel.getRoot().addProject(project, false);
}
}
public void deleteSessionProject(DBPProject project) {
public void deleteSessionProject(@Nullable WebProjectImpl project) {
getWorkspace().remoteProject((WebProjectImpl) project);
if (navigatorModel != null) {
navigatorModel.getRoot().removeProject(project);
}
}
@Override
public void addSessionProject(@NotNull String projectId) throws DBException {
super.addSessionProject(projectId);
var rmProject = getRmController().getProject(projectId, false, false);
createWebProject(rmProject);
}
@Override
public void removeSessionProject(@Nullable String projectId) throws DBException {
super.removeSessionProject(projectId);
var project = getProjectById(projectId);
if (project == null) {
return;
}
deleteSessionProject(project);
var projectConnections = project.getDataSourceRegistry().getDataSources();
for (DBPDataSourceContainer c : projectConnections) {
removeConnection(new WebConnectionInfo(this, c));
}
}
@Property
public boolean isValid() {
return getSessionActiveTimeLeft() > 0;
@@ -18,7 +18,7 @@ package io.cloudbeaver.model.session;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.websocket.event.WSAbstractProjectEvent;
import org.jkiss.dbeaver.model.websocket.event.WSProjectResourceEvent;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import java.util.Set;
@@ -55,8 +55,8 @@ public class WebSessionEventsFilter {
return false;
}
if (!subscribedProjectIds.isEmpty() && event instanceof WSAbstractProjectEvent) {
var projectEvent = (WSAbstractProjectEvent) event;
if (!subscribedProjectIds.isEmpty() && event instanceof WSProjectResourceEvent) {
var projectEvent = (WSProjectResourceEvent) event;
if (!subscribedProjectIds.contains(projectEvent.getProjectId())) {
return false;
}
@@ -49,6 +49,8 @@
<eventHandler class="io.cloudbeaver.server.events.WSDataSourceUpdatedEventHandlerImpl"/>
<eventHandler class="io.cloudbeaver.server.events.WSFolderUpdatedEventHandlerImpl"/>
<eventHandler class="io.cloudbeaver.server.events.WSRmResourceUpdatedEventHandlerImpl"/>
<eventHandler class="io.cloudbeaver.server.events.WSPermissionUpdatedEventHandler"/>
<eventHandler class="io.cloudbeaver.server.events.WSProjectUpdatedEventHandler"/>
<eventHandler class="io.cloudbeaver.server.events.WSUserSecretEventHandlerImpl"/>
</extension>
@@ -16,9 +16,12 @@ enum CBServerEventId {
cb_datasource_folder_updated,
cb_datasource_folder_deleted,
cb_rm_resource_created
cb_rm_resource_updated
cb_rm_resource_deleted
cb_rm_resource_created,
cb_rm_resource_updated,
cb_rm_resource_deleted,
cb_rm_project_added,
cb_rm_project_removed
}
# Events sent by client
@@ -86,12 +89,26 @@ type CBSessionLogEvent implements CBServerEvent {
topicId: CBEventTopic
}
# RM project update event
type CBProjectUpdateEvent implements CBServerEvent {
id: CBServerEventId!
topicId: CBEventTopic
projectId: String!
}
# Topic subscription event
type CBTopicEvent implements CBClientEvent {
id: CBClientEventId!
topicId: CBEventTopic! # topic to subscribe / unsubscribe
}
# Project event
type CBProjectEvent implements CBClientEvent {
id: CBClientEventId!
topicId: CBEventTopic
projectId: String!
}
# Active projects event
type CBProjectsActiveEvent implements CBClientEvent {
id: CBClientEventId!
@@ -0,0 +1,74 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2023 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.server.events;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
import org.jkiss.dbeaver.model.websocket.event.*;
import java.util.Collection;
/**
* Notify all active user session that project has been updated
*/
public abstract class WSAbstractProjectEventHandler<Event extends WSProjectEvent> implements WSEventHandler {
@NotNull
@Override
public String getSupportedTopicId() {
return WSEventTopic.PROJECTS.getTopicId();
}
@Override
public void handleEvent(@NotNull WSEvent event) {
if (!getEventClass().isInstance(event)) {
return;
}
var typedEvent = getEventClass().cast(event);
Collection<BaseWebSession> allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions();
for (var activeUserSession : allSessions) {
if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) {
continue; // skip events from current session
}
if (!validateEvent(activeUserSession, typedEvent)) {
getLog().debug(getSupportedTopicId() + " event '" + event.getId() + "' is not valid");
continue;
}
getLog().debug(getSupportedTopicId() + " event '" + event.getId() + "' handled");
updateSessionData(activeUserSession, typedEvent);
}
}
/**
* Method to get logger from implementation to display the implementation class name in the log
*/
@NotNull
protected abstract Log getLog();
@NotNull
protected abstract Class<Event> getEventClass();
protected abstract void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull Event event);
protected boolean validateEvent(@NotNull BaseWebSession activeUserSession, @NotNull Event event) {
return activeUserSession.isProjectAccessible(event.getProjectId()) &&
WSEventType.valueById(event.getId()) != null;
}
}
@@ -20,7 +20,7 @@ import io.cloudbeaver.WebProjectImpl;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.event.WSEventTopic;
import org.jkiss.dbeaver.model.websocket.event.WSEventType;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
@@ -28,34 +28,38 @@ import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
/**
* Notify all active user session that datasource has been updated
*/
public class WSDataSourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler {
public class WSDataSourceUpdatedEventHandlerImpl extends WSAbstractProjectEventHandler<WSDataSourceEvent> {
public static final Log log = Log.getLog(WSDataSourceUpdatedEventHandlerImpl.class);
@NotNull
@Override
public String getSupportedTopicId() {
return WSEventTopic.DATASOURCE.getTopicId();
}
@NotNull
@Override
protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) {
if (!(event instanceof WSDataSourceEvent)) {
return;
}
var dsUpdateEvent = (WSDataSourceEvent) event;
if (!activeUserSession.isProjectAccessible(dsUpdateEvent.getProjectId())) {
return;
}
var eventType = WSEventType.valueById(event.getId());
if (eventType == null) {
return;
}
protected Log getLog() {
return log;
}
@NotNull
@Override
protected Class<WSDataSourceEvent> getEventClass() {
return WSDataSourceEvent.class;
}
@Override
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDataSourceEvent event) {
var sendEvent = true;
if (activeUserSession instanceof WebSession) {
var webSession = (WebSession) activeUserSession;
WebProjectImpl project = webSession.getProjectById(dsUpdateEvent.getProjectId());
WebProjectImpl project = webSession.getProjectById(event.getProjectId());
sendEvent = webSession.updateProjectConnection(
project,
dsUpdateEvent.getDataSourceIds(),
eventType
event.getDataSourceIds(),
WSEventType.valueById(event.getId())
);
}
if (sendEvent) {
@@ -19,7 +19,7 @@ package io.cloudbeaver.server.events;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.event.WSEventTopic;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent;
import org.jkiss.utils.CommonUtils;
@@ -27,31 +27,42 @@ import org.jkiss.utils.CommonUtils;
/**
* Notify all active user session that datasource has been updated
*/
public class WSFolderUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler {
public class WSFolderUpdatedEventHandlerImpl extends WSAbstractProjectEventHandler<WSDatasourceFolderEvent> {
private static final Log log = Log.getLog(WSFolderUpdatedEventHandlerImpl.class);
@NotNull
@Override
public String getSupportedTopicId() {
return WSEventTopic.DATASOURCE_FOLDER.getTopicId();
}
@NotNull
@Override
protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) {
if (!(event instanceof WSDatasourceFolderEvent)) {
return;
}
var dsFolderUpdateEvent = (WSDatasourceFolderEvent) event;
boolean isProjectAccessible = activeUserSession.isProjectAccessible(dsFolderUpdateEvent.getProjectId());
if (!isProjectAccessible || CommonUtils.isEmpty(dsFolderUpdateEvent.getNodePaths())) {
return;
}
protected Log getLog() {
return log;
}
@NotNull
@Override
protected Class<WSDatasourceFolderEvent> getEventClass() {
return WSDatasourceFolderEvent.class;
}
@Override
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSDatasourceFolderEvent event) {
if (activeUserSession instanceof WebSession) {
var webSession = (WebSession) activeUserSession;
var project = webSession.getProjectById(dsFolderUpdateEvent.getProjectId());
var project = webSession.getProjectById(event.getProjectId());
project.getDataSourceRegistry().refreshConfig();
webSession.getNavigatorModel().getRoot().getProjectNode(project).getDatabases().refreshChildren();
}
activeUserSession.addSessionEvent(event);
}
@Override
protected boolean validateEvent(@NotNull BaseWebSession activeUserSession, @NotNull WSDatasourceFolderEvent event) {
return !CommonUtils.isEmpty(event.getNodePaths()) && super.validateEvent(activeUserSession, event);
}
}
@@ -0,0 +1,110 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2023 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.server.events;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant;
import org.jkiss.dbeaver.model.security.SMObjects;
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import org.jkiss.dbeaver.model.websocket.event.WSEventTopic;
import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent;
import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class WSPermissionUpdatedEventHandler implements WSEventHandler {
private static final Log log = Log.getLog(WSPermissionUpdatedEventHandler.class);
@NotNull
@Override
public String getSupportedTopicId() {
return WSEventTopic.PERMISSIONS.getTopicId();
}
@Override
public void handleEvent(@NotNull WSEvent event) {
if (!(event instanceof WSObjectPermissionEvent)) {
return;
}
var permissionEvent = (WSObjectPermissionEvent) event;
if (!SMObjects.PROJECT_OBJECT_TYPE_ID.equals(permissionEvent.getSmObjectType().getObjectType())) {
return;
}
var smController = CBPlatform.getInstance().getApplication().getSecurityController();
try {
var projectId = permissionEvent.getObjectId();
var permissionGrants = smController.getObjectPermissionGrants(projectId, permissionEvent.getSmObjectType());
var subjectsWithProjectAccess = permissionGrants.stream()
.map(SMObjectPermissionsGrant::getSubjectId)
.collect(Collectors.toSet());
log.debug(getSupportedTopicId() + " event handled");
Collection<BaseWebSession> allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions();
for (var activeUserSession : allSessions) {
if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) {
continue;
}
var user = activeUserSession.getUserContext().getUser();
if (user == null) {
continue;
}
var userSubjects = new HashSet<>(Set.of(user.getTeams()));
userSubjects.add(user.getUserId());
var accessibleProjectIds = activeUserSession.getUserContext().getAccessibleProjectIds();
var isAccessibleNow = accessibleProjectIds.contains(projectId);
var shouldBeAccessible = subjectsWithProjectAccess.stream().anyMatch(userSubjects::contains);
if (shouldBeAccessible && !isAccessibleNow) {
// adding project to session cache
activeUserSession.addSessionProject(projectId);
activeUserSession.addSessionEvent(
WSProjectUpdateEvent.create(
event.getSessionId(),
event.getUserId(),
projectId
)
);
} else if (!shouldBeAccessible && isAccessibleNow) {
// removing project from session cache
activeUserSession.removeSessionProject(projectId);
activeUserSession.addSessionEvent(
WSProjectUpdateEvent.delete(
event.getSessionId(),
event.getUserId(),
projectId
)
);
}
}
} catch (DBException e) {
log.error("Cannot get permission grants", e);
}
}
}
@@ -16,38 +16,58 @@
*/
package io.cloudbeaver.server.events;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
import org.jkiss.dbeaver.model.websocket.WSUtils;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import org.jkiss.utils.CommonUtils;
import org.jkiss.dbeaver.model.websocket.event.WSEventTopic;
import org.jkiss.dbeaver.model.websocket.event.WSEventType;
import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent;
import java.util.Collection;
public class WSProjectUpdatedEventHandler extends WSAbstractProjectEventHandler<WSProjectUpdateEvent> {
/**
* Notify all active user session that project has been updated
*/
public abstract class WSProjectUpdatedEventHandler implements WSEventHandler {
private static final Log log = Log.getLog(WSProjectUpdatedEventHandler.class);
@NotNull
@Override
public abstract String getSupportedTopicId();
public String getSupportedTopicId() {
return WSEventTopic.PROJECTS.getTopicId();
}
@NotNull
@Override
protected Log getLog() {
return log;
}
@NotNull
@Override
protected Class<WSProjectUpdateEvent> getEventClass() {
return WSProjectUpdateEvent.class;
}
@Override
public void handleEvent(@NotNull WSEvent event) {
log.debug(getSupportedTopicId() + " event handled");
Collection<BaseWebSession> allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions();
for (var activeUserSession : allSessions) {
if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) {
continue;
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSProjectUpdateEvent event) {
var eventId = event.getId();
var projectId = event.getProjectId();
try {
if (eventId.equals(WSEventType.RM_PROJECT_ADDED.getEventId())) {
activeUserSession.addSessionProject(projectId);
log.info("Project '" + projectId + "' added to '" + activeUserSession.getSessionId() + "' session");
} else if (eventId.equals(WSEventType.RM_PROJECT_REMOVED.getEventId())) {
activeUserSession.removeSessionProject(projectId);
log.info("Project '" + projectId + "' removed from '" + activeUserSession.getSessionId() + "' session");
}
updateSessionData(activeUserSession, event);
activeUserSession.addSessionEvent(event);
} catch (DBException e) {
log.warn("Failed to handle project lifecycle event", e);
}
}
protected abstract void updateSessionData(BaseWebSession activeUserSession, WSEvent event);
@Override
protected boolean validateEvent(@NotNull BaseWebSession activeUserSession, @NotNull WSProjectUpdateEvent event) {
return event.getId().equals(WSEventType.RM_PROJECT_REMOVED.getEventId()) ||
activeUserSession.getUserContext().hasPermission(DBWConstants.PERMISSION_ADMIN);
}
}
@@ -22,10 +22,10 @@ import io.cloudbeaver.WebProjectImpl;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.rm.RMEvent;
import org.jkiss.dbeaver.model.rm.RMEventManager;
import org.jkiss.dbeaver.model.rm.RMResource;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import org.jkiss.dbeaver.model.websocket.event.WSEventTopic;
import org.jkiss.dbeaver.model.websocket.event.WSEventType;
import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceUpdatedEvent;
@@ -36,8 +36,9 @@ import java.util.List;
/**
* Notify all active user session that rm resource has been updated
*/
public class WSRmResourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler {
public class WSRmResourceUpdatedEventHandlerImpl extends WSAbstractProjectEventHandler<WSResourceUpdatedEvent> {
private static final Log log = Log.getLog(WSRmResourceUpdatedEventHandlerImpl.class);
private static final Gson gson = new GsonBuilder().create();
@NotNull
@@ -46,33 +47,31 @@ public class WSRmResourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHa
return WSEventTopic.RM_SCRIPTS.getTopicId();
}
@NotNull
@Override
protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) {
if (!(event instanceof WSResourceUpdatedEvent)) {
return;
}
var resourceUpdateEvent = (WSResourceUpdatedEvent) event;
String projectId = resourceUpdateEvent.getProjectId();
if (!activeUserSession.isProjectAccessible(projectId)) {
return;
}
if (resourceUpdateEvent.getResourcePath() == null) {
return;
}
Object parsedResourcePath = resourceUpdateEvent.getResourceParsedPath();
RMResource[] resourceParsedPath;
if (parsedResourcePath instanceof RMResource[]) {
resourceParsedPath = (RMResource[]) parsedResourcePath;
} else {
resourceParsedPath = gson.fromJson(gson.toJson(parsedResourcePath), RMResource[].class);
}
var eventType = WSEventType.valueById(resourceUpdateEvent.getId());
if (eventType == null) {
return;
}
protected Log getLog() {
return log;
}
@NotNull
@Override
protected Class<WSResourceUpdatedEvent> getEventClass() {
return WSResourceUpdatedEvent.class;
}
@Override
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSResourceUpdatedEvent event) {
if (activeUserSession instanceof WebSession) {
var parsedResourcePath = event.getResourceParsedPath();
var resourceParsedPath = parsedResourcePath instanceof RMResource[]
? (RMResource[]) parsedResourcePath
: gson.fromJson(gson.toJson(parsedResourcePath), RMResource[].class);
var webSession = (WebSession) activeUserSession;
acceptChangesInNavigatorTree(eventType, resourceParsedPath, webSession.getProjectById(projectId));
acceptChangesInNavigatorTree(
WSEventType.valueById(event.getId()),
resourceParsedPath,
webSession.getProjectById(event.getProjectId())
);
}
activeUserSession.addSessionEvent(event);
}
@@ -18,20 +18,26 @@ package io.cloudbeaver.server.events;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
import org.jkiss.dbeaver.model.websocket.event.WSEventTopic;
import org.jkiss.dbeaver.model.websocket.event.WSUserSecretEvent;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import java.util.Collection;
import java.util.List;
/**
* Notify all active user session that rm resource has been updated
*/
public class WSUserSecretEventHandlerImpl extends WSProjectUpdatedEventHandler {
public class WSUserSecretEventHandlerImpl implements WSEventHandler {
private static final Log log = Log.getLog(WSUserSecretEventHandlerImpl.class);
@NotNull
@Override
@@ -40,6 +46,16 @@ public class WSUserSecretEventHandlerImpl extends WSProjectUpdatedEventHandler {
}
@Override
public void handleEvent(@NotNull WSEvent event) {
Collection<BaseWebSession> allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions();
for (var activeUserSession : allSessions) {
if (WSWebUtils.isSessionIdEquals(activeUserSession, event.getSessionId())) {
continue; // skip events from current session
}
updateSessionData(activeUserSession, event);
}
}
protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) {
if (!(event instanceof WSUserSecretEvent && activeUserSession instanceof WebSession)) {
return;
@@ -23,16 +23,18 @@ import io.cloudbeaver.service.rm.DBWServiceRM;
import io.cloudbeaver.service.rm.model.RMProjectPermissions;
import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions;
import io.cloudbeaver.service.security.SMUtils;
import io.cloudbeaver.utils.WebAppUtils;
import io.cloudbeaver.utils.WebEventUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.app.DBPProject;
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.security.*;
import org.jkiss.dbeaver.model.websocket.WSConstants;
import org.jkiss.dbeaver.model.websocket.event.WSProjectUpdateEvent;
import org.jkiss.dbeaver.model.websocket.event.permissions.WSObjectPermissionEvent;
import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceProperty;
import java.nio.charset.StandardCharsets;
@@ -255,6 +257,9 @@ public class WebServiceRM implements DBWServiceRM {
try {
RMProject rmProject = getResourceController(session).createProject(name, description);
session.createWebProject(rmProject);
WebAppUtils.getWebApplication().getEventController().addEvent(
WSProjectUpdateEvent.create(session.getSessionId(), session.getUserId(), rmProject.getId())
);
return rmProject;
} catch (DBException e) {
throw new DBWebException("Error creating project", e);
@@ -264,9 +269,12 @@ public class WebServiceRM implements DBWServiceRM {
@Override
public boolean deleteProject(@NotNull WebSession session, @NotNull String projectId) throws DBWebException {
try {
DBPProject project = session.getProjectById(projectId);
var project = session.getProjectById(projectId);
getResourceController(session).deleteProject(projectId);
session.deleteSessionProject(project);
WebAppUtils.getWebApplication().getEventController().addEvent(
WSProjectUpdateEvent.delete(session.getSessionId(), session.getUserId(), projectId)
);
return true;
} catch (DBException e) {
throw new DBWebException("Error deleting project", e);
@@ -301,12 +309,26 @@ public class WebServiceRM implements DBWServiceRM {
webSession.getUserId()
);
}
addProjectPermissionsUpdatedEvent(webSession, projectId);
return true;
} catch (Exception e) {
throw new DBWebException("Error granting project permissions", e);
}
}
private void addProjectPermissionsUpdatedEvent(
@NotNull WebSession webSession,
@NotNull String projectId
) {
var event = WSObjectPermissionEvent.update(
webSession.getUserContext().getSmSessionId(),
webSession.getUserId(),
SMObjects.PROJECT,
projectId
);
webSession.getApplication().getEventController().addEvent(event);
}
@Override
public boolean setSubjectProjectPermissions(
@NotNull WebSession webSession,
@@ -325,6 +347,7 @@ public class WebServiceRM implements DBWServiceRM {
permissions,
webSession.getUserId()
);
addProjectPermissionsUpdatedEvent(webSession, projectId);
}
return true;
} catch (Exception e) {
@@ -7,11 +7,11 @@
*/
import { injectable } from '@cloudbeaver/core-di';
import { SessionEventSource, TopicEventHandler, ISessionEvent, ClientEventId, SessionEventTopic, IBaseServerEvent, SessionEventId } from '@cloudbeaver/core-root';
import type { CbProjectsActiveEvent } from '@cloudbeaver/core-sdk';
import { SessionEventSource, TopicEventHandler, ISessionEvent, ClientEventId, SessionEventTopic, SessionEventId } from '@cloudbeaver/core-root';
import type { CbProjectsActiveEvent, CbProjectUpdateEvent as IProjectInfoEvent } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
export type IProjectInfoEvent = IBaseServerEvent<SessionEventId, SessionEventTopic>;
export { IProjectInfoEvent };
@injectable()
export class ProjectInfoEventHandler
@@ -38,7 +38,7 @@ export class ProjectInfoEventHandler
this.lastActiveProjects = projectIds;
}
map(event: ISessionEvent): IProjectInfoEvent {
map(event: any): IProjectInfoEvent {
return event;
}
}
@@ -11,12 +11,13 @@ import { computed, makeObservable } from 'mobx';
import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication';
import { Dependency, injectable } from '@cloudbeaver/core-di';
import { Executor, ExecutorInterrupter, IExecutor, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-sdk';
import { DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root';
import { CachedMapAllKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { NavigationService } from '@cloudbeaver/core-ui';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { activeProjectsContext } from './activeProjectsContext';
import { ProjectInfoEventHandler } from './ProjectInfoEventHandler';
import { IProjectInfoEvent, ProjectInfoEventHandler } from './ProjectInfoEventHandler';
import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource';
interface IActiveProjectData {
@@ -95,6 +96,7 @@ export class ProjectsService extends Dependency {
private readonly userInfoResource: UserInfoResource,
private readonly userDataService: UserDataService,
private readonly projectInfoEventHandler: ProjectInfoEventHandler,
private readonly dataSynchronizationService: DataSynchronizationService,
navigationService: NavigationService
) {
super();
@@ -127,6 +129,34 @@ export class ProjectsService extends Dependency {
this.projectInfoEventHandler.setActiveProjects(this.activeProjects.map(project => project.id));
});
this.projectInfoResource.onItemDelete.addHandler(async data => {
const ids = ResourceKeyUtils.toArray(data);
const wasActive = ids.some(id => this.activeProjectIds.includes(id));
if (wasActive) {
await this.setActiveProjects(this.activeProjects.filter(project => !ids.includes(project.id)));
}
});
this.projectInfoEventHandler.onEvent<IProjectInfoEvent>(ServerEventId.CbRmProjectAdded, () => {
this.projectInfoResource.markOutdated();
}, undefined, this.projectInfoResource);
this.projectInfoEventHandler.onEvent<IProjectInfoEvent>(ServerEventId.CbRmProjectRemoved, key => {
if (this.activeProjectIds.includes(key.projectId)) {
const project = this.projectInfoResource.get(key.projectId);
this.dataSynchronizationService
.requestSynchronization('project', project?.name ?? '')
.then(state => {
if (state) {
this.projectInfoResource.delete(key.projectId);
}
});
} else {
this.projectInfoResource.delete(key.projectId);
}
}, undefined, this.projectInfoResource);
makeObservable(this, {
userProject: computed,
defaultProject: computed,
+14
View File
@@ -205,6 +205,18 @@ export enum CbEventTopic {
CbSessionLog = 'cb_session_log'
}
export interface CbProjectEvent extends CbClientEvent {
id: CbClientEventId;
projectId: Scalars['String'];
topicId?: Maybe<CbEventTopic>;
}
export interface CbProjectUpdateEvent extends CbServerEvent {
id: CbServerEventId;
projectId: Scalars['String'];
topicId?: Maybe<CbEventTopic>;
}
export interface CbProjectsActiveEvent extends CbClientEvent {
id: CbClientEventId;
projectIds: Array<Scalars['String']>;
@@ -231,6 +243,8 @@ export enum CbServerEventId {
CbDatasourceFolderDeleted = 'cb_datasource_folder_deleted',
CbDatasourceFolderUpdated = 'cb_datasource_folder_updated',
CbDatasourceUpdated = 'cb_datasource_updated',
CbRmProjectAdded = 'cb_rm_project_added',
CbRmProjectRemoved = 'cb_rm_project_removed',
CbRmResourceCreated = 'cb_rm_resource_created',
CbRmResourceDeleted = 'cb_rm_resource_deleted',
CbRmResourceUpdated = 'cb_rm_resource_updated',
@@ -14,7 +14,7 @@ import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '@cloudbe
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { type NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { type NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import type { IDNDData } from '@cloudbeaver/core-ui';
@@ -81,7 +81,7 @@ export interface IElementsTreeOptions {
isGroup?: (node: NavNode) => boolean;
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
beforeSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
customSelectReset?: ()=> Promise<void> | void;
customSelectReset?: () => Promise<void> | void;
}
interface IOptions extends IElementsTreeOptions {
@@ -125,6 +125,7 @@ export interface IElementsTree extends ILoadableState {
export function useElementsTree(options: IOptions): IElementsTree {
const projectsService = useService(ProjectsService);
const projectInfoResource = useService(ProjectInfoResource);
const notificationService = useService(NotificationService);
const navNodeInfoResource = useService(NavNodeInfoResource);
const navTreeResource = useService(NavTreeResource);
@@ -618,6 +619,11 @@ export function useElementsTree(options: IOptions): IElementsTree {
}],
});
useExecutor({
executor: projectInfoResource.onDataOutdated,
handlers: [() => navTreeResource.markOutdated(ROOT_NODE_PATH)],
});
useExecutor({
executor: navTreeResource.onItemUpdate,
handlers: [function exitFolder(key) {