diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF
index e0b4ced494..72a1fe2221 100644
--- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF
+++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF
@@ -20,6 +20,7 @@ Export-Package: io.cloudbeaver,
io.cloudbeaver.auth.provider,
io.cloudbeaver.auth.provider.fa,
io.cloudbeaver.auth.provider.local,
+ io.cloudbeaver.events,
io.cloudbeaver.model,
io.cloudbeaver.model.app,
io.cloudbeaver.model.rm,
diff --git a/server/bundles/io.cloudbeaver.model/plugin.xml b/server/bundles/io.cloudbeaver.model/plugin.xml
index 572b6e6f11..1bbc06fb18 100644
--- a/server/bundles/io.cloudbeaver.model/plugin.xml
+++ b/server/bundles/io.cloudbeaver.model/plugin.xml
@@ -3,4 +3,5 @@
+
diff --git a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd
new file mode 100644
index 0000000000..93269606b0
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+ Web events handler
+
+
+
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java
new file mode 100644
index 0000000000..635d835d50
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java
@@ -0,0 +1,50 @@
+/*
+ * 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.events;
+
+import org.jkiss.code.NotNull;
+
+import java.util.Map;
+
+/**
+ * CloudBeaver event
+ */
+public class CBEvent {
+ @NotNull
+ private final String eventType;
+ @NotNull
+ private final Map eventData;
+
+ public CBEvent(@NotNull String eventType) {
+ this(eventType, Map.of());
+ }
+
+ public CBEvent(@NotNull String eventType, @NotNull Map eventData) {
+ this.eventType = eventType;
+ this.eventData = eventData;
+ }
+
+ @NotNull
+ public String getEventType() {
+ return eventType;
+ }
+
+ @NotNull
+ public Map getEventData() {
+ return eventData;
+ }
+}
\ No newline at end of file
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java
new file mode 100644
index 0000000000..35aabe6f19
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java
@@ -0,0 +1,24 @@
+/*
+ * 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.events;
+
+/**
+ * CB event constants
+ */
+public interface CBEventConstants {
+ String CLOUDBEAVER_CONFIG_CHANGED = "cb_config_changed";
+}
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java
new file mode 100644
index 0000000000..011d83c75f
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java
@@ -0,0 +1,86 @@
+/*
+ * 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.events;
+
+import io.cloudbeaver.events.registry.CBEventHandlersRegistry;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.jkiss.code.NotNull;
+import org.jkiss.dbeaver.model.runtime.AbstractJob;
+import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CBEventController {
+ private final Map> eventHandlersByType = new HashMap<>();
+
+ public CBEventController() {
+ var eventHandlers = CBEventHandlersRegistry.getInstance().getEventHandlers();
+
+ eventHandlers
+ .forEach(handler -> eventHandlersByType.computeIfAbsent(handler.getSupportedEventType(), x -> new ArrayList<>()).add(handler));
+ }
+
+ private final List eventsPool = new ArrayList<>();
+
+ /**
+ * Add cb event to the event pool
+ */
+ public void addEvent(@NotNull CBEvent event) {
+ synchronized (eventsPool) {
+ eventsPool.add(event);
+ }
+ }
+
+ /**
+ * Add cb event to the event pool
+ */
+ public void scheduleCheckJob() {
+ new CBEventCheckJob().schedule();
+ }
+
+ private class CBEventCheckJob extends AbstractJob {
+ private static final long CHECK_PERIOD = 1000;
+
+ protected CBEventCheckJob() {
+ super("CloudBeaver events job");
+ }
+
+ @Override
+ protected IStatus run(DBRProgressMonitor monitor) {
+ List events;
+
+ synchronized (eventsPool) {
+ events = List.copyOf(eventsPool);
+ eventsPool.clear();
+ }
+ if (events.isEmpty()) {
+ schedule(CHECK_PERIOD);
+ return Status.OK_STATUS;
+ }
+ for (CBEvent event : events) {
+ eventHandlersByType.getOrDefault(event.getEventType(), List.of())
+ .forEach(handler -> handler.handleEvent(event));
+ }
+ schedule(CHECK_PERIOD);
+ return Status.OK_STATUS;
+ }
+ }
+}
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java
new file mode 100644
index 0000000000..574c0082fa
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.events;
+
+import org.jkiss.code.NotNull;
+
+/**
+ * CB web event handlers
+ */
+public interface CBEventHandler {
+ /**
+ * Expected cb event type
+ */
+ @NotNull
+ String getSupportedEventType();
+
+ /**
+ * Handle associated cb event
+ */
+ void handleEvent(@NotNull CBEvent event);
+}
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java
new file mode 100644
index 0000000000..65fad36ee6
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java
@@ -0,0 +1,44 @@
+/*
+ * 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.events.registry;
+
+import io.cloudbeaver.events.CBEventHandler;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.jkiss.code.NotNull;
+import org.jkiss.dbeaver.DBException;
+import org.jkiss.dbeaver.model.impl.AbstractDescriptor;
+
+/**
+ * CB event handler descriptor
+ */
+public class CBEventHandlerDescriptor extends AbstractDescriptor {
+ private final ObjectType implType;
+
+ protected CBEventHandlerDescriptor(IConfigurationElement contributorConfig) {
+ super(contributorConfig);
+ this.implType = new ObjectType(contributorConfig, "class");
+ }
+
+ @NotNull
+ public CBEventHandler getInstance() {
+ try {
+ return implType.createInstance(CBEventHandler.class);
+ } catch (DBException e) {
+ throw new IllegalStateException("Can not instantiate event handler '" + implType.getImplName() + "'", e);
+ }
+ }
+}
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java
new file mode 100644
index 0000000000..6cd82eb415
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java
@@ -0,0 +1,61 @@
+/*
+ * 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.events.registry;
+
+import io.cloudbeaver.events.CBEventHandler;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.core.runtime.Platform;
+import org.jkiss.code.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CBEventHandlersRegistry {
+ private static final String EXTENSION_ID = "io.cloudbeaver.event.handler";
+ private static final String EVENT_HANDLER_TAG = "eventHandler";
+ private static CBEventHandlersRegistry instance = null;
+
+ public synchronized static CBEventHandlersRegistry getInstance() {
+ if (instance == null) {
+ instance = new CBEventHandlersRegistry();
+ }
+ return instance;
+ }
+
+ @NotNull
+ public List getEventHandlers() {
+ List eventHandlerDescriptors = readDescriptors();
+
+ return eventHandlerDescriptors.stream()
+ .map(CBEventHandlerDescriptor::getInstance)
+ .collect(Collectors.toList());
+ }
+
+ @NotNull
+ private List readDescriptors() {
+ var result = new ArrayList();
+ var registry = Platform.getExtensionRegistry();
+ for (IConfigurationElement ext : registry.getConfigurationElementsFor(EXTENSION_ID)) {
+ // Load webServices
+ if (EVENT_HANDLER_TAG.equals(ext.getName())) {
+ result.add(new CBEventHandlerDescriptor(ext));
+ }
+ }
+ return result;
+ }
+}
diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java
index aec7166ab2..99f2d8d325 100644
--- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java
+++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.DataSourceFilter;
import io.cloudbeaver.VirtualProjectImpl;
+import io.cloudbeaver.events.CBEvent;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.WebConnectionInfo;
import io.cloudbeaver.model.WebServerMessage;
@@ -124,6 +125,7 @@ public class WebSession extends AbstractSessionPersistent
private final WebApplication application;
private final Map sessionHandlers;
private final WebUserContext userContext;
+ private final List sessionEvents = new ArrayList<>();
public WebSession(HttpSession httpSession,
WebApplication application,
@@ -723,6 +725,27 @@ public class WebSession extends AbstractSessionPersistent
addSessionMessage(new WebServerMessage(WebServerMessage.MessageType.INFO, message));
}
+ public void addSessionEvent(CBEvent event) {
+ synchronized (sessionEvents) {
+ sessionEvents.add(event);
+ }
+ }
+
+ public List getSessionEvents(int eventsCount) {
+ synchronized (sessionEvents) {
+ List result;
+ if (sessionEvents.size() <= eventsCount) {
+ result = List.copyOf(sessionEvents);
+ sessionEvents.clear();
+ } else {
+ var subList = sessionEvents.subList(0, eventsCount - 1);
+ result = List.copyOf(subList);
+ subList.clear();
+ }
+ return result;
+ }
+ }
+
public List readLog(Integer maxEntries, Boolean clearLog) {
synchronized (sessionMessages) {
List messages = new ArrayList<>();
diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml
index 93a4bb342e..48e94d9dbf 100644
--- a/server/bundles/io.cloudbeaver.server/plugin.xml
+++ b/server/bundles/io.cloudbeaver.server/plugin.xml
@@ -15,6 +15,7 @@
+
@@ -22,13 +23,16 @@
-
+
-
+
-
+
@@ -44,4 +48,8 @@
+
+
+
+
diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls
index b753393dfe..d8652e7f17 100644
--- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls
+++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls
@@ -386,6 +386,15 @@ type LogEntry {
stackTrace: String
}
+enum CBEventType {
+ cb_config_changed
+}
+
+type CBEvent {
+ eventType: CBEventType!
+ eventData: Object!
+}
+
####################################################
# Input types
####################################################
@@ -503,6 +512,8 @@ extend type Query {
listProjects: [ ProjectInfo! ]!
readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ]!
+
+ readSessionEvents(maxEntries: Int!): [ CBEvent! ]!
}
extend type Mutation {
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java
index be1a1ea151..7d4ff0593d 100644
--- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java
@@ -21,6 +21,9 @@ import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.auth.NoAuthCredentialsProvider;
+import io.cloudbeaver.events.CBEvent;
+import io.cloudbeaver.events.CBEventConstants;
+import io.cloudbeaver.events.CBEventController;
import io.cloudbeaver.model.app.BaseWebApplication;
import io.cloudbeaver.model.app.WebAuthApplication;
import io.cloudbeaver.model.app.WebAuthConfiguration;
@@ -121,6 +124,8 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
private String localHostAddress;
private final List localInetAddresses = new ArrayList<>();
+ protected final CBEventController eventController = new CBEventController();
+
private WebSessionManager sessionManager;
public CBApplication() {
@@ -342,6 +347,8 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
System.setSecurityManager(new SecurityManager());
}
+ eventController.scheduleCheckJob();
+
runWebServer();
log.debug("Shutdown");
@@ -440,7 +447,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
@NotNull
private File getRuntimeAppConfigFile() {
- return getDataDirectory(true).resolve(CBConstants.RUNTIME_APP_CONFIG_FILE_NAME).toFile();
+ return getDataDirectory(true).resolve(CBConstants.RUNTIME_APP_CONFIG_FILE_NAME).toFile();
}
@NotNull
@@ -789,6 +796,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
}
configurationMode = CommonUtils.isEmpty(serverName);
+ eventController.addEvent(new CBEvent(CBEventConstants.CLOUDBEAVER_CONFIG_CHANGED));
}
protected Map readRuntimeConfigurationProperties() throws DBException {
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java
new file mode 100644
index 0000000000..9bafc539aa
--- /dev/null
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java
@@ -0,0 +1,51 @@
+/*
+ * 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.server.events;
+
+import io.cloudbeaver.events.CBEvent;
+import io.cloudbeaver.events.CBEventConstants;
+import io.cloudbeaver.events.CBEventHandler;
+import io.cloudbeaver.model.session.WebSession;
+import io.cloudbeaver.server.CBPlatform;
+import io.cloudbeaver.service.session.WebSessionManager;
+import org.jkiss.code.NotNull;
+import org.jkiss.dbeaver.Log;
+
+import java.util.Collection;
+
+/**
+ * Notify all active user session that configuration has been changed
+ */
+public class CBConfigurationChangedEventHandler implements CBEventHandler {
+ private static final Log log = Log.getLog(CBConfigurationChangedEventHandler.class);
+
+ @NotNull
+ @Override
+ public String getSupportedEventType() {
+ return CBEventConstants.CLOUDBEAVER_CONFIG_CHANGED;
+ }
+
+ @Override
+ public void handleEvent(@NotNull CBEvent event) {
+ log.debug(getSupportedEventType() + " event handled");
+ Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions();
+
+ for (WebSession activeUserSession : allSessions) {
+ activeUserSession.addSessionEvent(event);
+ }
+ }
+}
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java
index fc8346c619..666d0f33a9 100644
--- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebAction;
import io.cloudbeaver.WebObjectId;
import io.cloudbeaver.WebProjectAction;
+import io.cloudbeaver.events.CBEvent;
import io.cloudbeaver.model.*;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.DBWService;
@@ -84,6 +85,9 @@ public interface DBWServiceCore extends DBWService {
@WebAction
List readSessionLog(@NotNull WebSession webSession, Integer maxEntries, Boolean clearEntries) throws DBWebException;
+ @WebAction(requirePermissions = {})
+ List readSessionEvents(@Nullable WebSession webSession, Integer maxEntries) throws DBWebException;
+
@WebAction(requirePermissions = {})
boolean closeSession(HttpServletRequest request) throws DBWebException;
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java
index 2328f86ac9..7dba9a5676 100644
--- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java
@@ -73,7 +73,9 @@ public class WebServiceBindingCore extends WebServiceBindingBase
getWebSession(env), getProjectReference(env), env.getArgument("id")))
.dataFetcher("listProjects", env -> getService(env).getProjects(getWebSession(env)))
-
+ .dataFetcher("readSessionEvents", env -> getService(env).readSessionEvents(
+ findWebSession(env), env.getArgument("maxEntries")
+ ))
.dataFetcher("readSessionLog", env -> {
// CB-90. Log read mustn't extend session lifetime and mustn't fail if there is no session.
WebSession session = findWebSession(env);
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java
index 204cd5b918..7889510c17 100644
--- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java
@@ -20,6 +20,7 @@ package io.cloudbeaver.service.core.impl;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
+import io.cloudbeaver.events.CBEvent;
import io.cloudbeaver.model.*;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.registry.WebHandlerRegistry;
@@ -51,6 +52,7 @@ import org.jkiss.dbeaver.model.net.DBWTunnel;
import org.jkiss.dbeaver.model.net.ssh.SSHImplementation;
import org.jkiss.dbeaver.model.rm.RMProjectType;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
+import org.jkiss.dbeaver.registry.BaseProjectImpl;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
@@ -177,12 +179,25 @@ public class WebServiceCore implements DBWServiceCore {
public List getConnectionFolders(
@NotNull WebSession webSession, @Nullable String projectId, @Nullable String id
) throws DBWebException {
+ if (projectId == null) {
+ return webSession.getAccessibleProjects().stream()
+ .flatMap(pr -> getConnectionFoldersFromProject(webSession, pr).stream())
+ .collect(Collectors.toList());
+ }
if (id != null) {
WebConnectionFolderInfo folderInfo = WebConnectionFolderUtils.getFolderInfo(webSession, projectId, id);
return Collections.singletonList(folderInfo);
}
- return webSession.getProjectById(projectId).getDataSourceRegistry().getAllFolders().stream()
- .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList());
+ DBPProject project = webSession.getProjectById(projectId);
+ return getConnectionFoldersFromProject(webSession, project);
+ }
+
+ private List getConnectionFoldersFromProject(
+ @NotNull WebSession webSession,
+ @NotNull DBPProject project
+ ) {
+ return project.getDataSourceRegistry().getAllFolders().stream()
+ .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList());
}
@Override
@@ -225,6 +240,14 @@ public class WebServiceCore implements DBWServiceCore {
return webSession.readLog(maxEntries, clearEntries);
}
+ @Override
+ public List readSessionEvents(@Nullable WebSession webSession, Integer maxEntries) throws DBWebException {
+ if (webSession == null) {
+ return Collections.emptyList();
+ }
+ return webSession.getSessionEvents(maxEntries);
+ }
+
@Override
public boolean closeSession(HttpServletRequest request) throws DBWebException {
try {
diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java
index 2d074040dc..cfa5ead58a 100644
--- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java
+++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java
@@ -188,4 +188,9 @@ public class WebSessionManager {
}
}
+ public Collection getAllActiveSessions() {
+ synchronized (sessionMap) {
+ return sessionMap.values();
+ }
+ }
}
diff --git a/webapp/packages/core-root/src/ServerConfigEventHandler.ts b/webapp/packages/core-root/src/ServerConfigEventHandler.ts
new file mode 100644
index 0000000000..4d76e34da3
--- /dev/null
+++ b/webapp/packages/core-root/src/ServerConfigEventHandler.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { injectable } from '@cloudbeaver/core-di';
+import {
+ CbEvent,
+ ResourceEventHandler
+} from '@cloudbeaver/core-sdk';
+
+import { SessionEvent, SessionEventSource, SessionEventType } from './SessionEventSource';
+
+@injectable()
+export class ServerConfigEventHandler extends ResourceEventHandler {
+ constructor(
+ sessionEventSource: SessionEventSource
+ ) {
+ super(sessionEventSource);
+ }
+
+ map(event: CbEvent): null {
+ return null;
+ }
+
+ filter(event: CbEvent): boolean {
+ return event.eventType === SessionEventType.CbConfigChanged;
+ }
+}
diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts
index 3476c6aeb3..4c22f929c2 100644
--- a/webapp/packages/core-root/src/ServerConfigResource.ts
+++ b/webapp/packages/core-root/src/ServerConfigResource.ts
@@ -13,6 +13,7 @@ import { GraphQLService, CachedDataResource, ServerConfig, ServerConfigInput, Na
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { isNavigatorViewSettingsEqual } from './ConnectionNavigatorViewSettings';
+import { ServerConfigEventHandler } from './ServerConfigEventHandler';
@injectable()
export class ServerConfigResource extends CachedDataResource {
@@ -20,7 +21,8 @@ export class ServerConfigResource extends CachedDataResource undefined);
}
get redirectOnFederatedAuth(): boolean {
diff --git a/webapp/packages/core-root/src/SessionEventSource.ts b/webapp/packages/core-root/src/SessionEventSource.ts
new file mode 100644
index 0000000000..327a760520
--- /dev/null
+++ b/webapp/packages/core-root/src/SessionEventSource.ts
@@ -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 { injectable } from '@cloudbeaver/core-di';
+import {
+ GraphQLService,
+ EventSource,
+ CbEvent,
+ CbEventType as SessionEventType
+} from '@cloudbeaver/core-sdk';
+
+export type SessionEvent = CbEvent;
+export { SessionEventType };
+
+@injectable()
+export class SessionEventSource extends EventSource {
+ constructor(
+ private readonly graphQLService: GraphQLService
+ ) {
+ super(5000);
+ }
+ protected async listener(): Promise {
+ const { events } = await this.graphQLService.sdk.getSessionEvents({
+ maxEntries: 1000,
+ });
+
+ for (const event of events) {
+ this.event(event);
+ }
+ }
+}
diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts
index 7dfcb262bf..7c01cb70d7 100644
--- a/webapp/packages/core-root/src/manifest.ts
+++ b/webapp/packages/core-root/src/manifest.ts
@@ -12,11 +12,13 @@ import { FeaturesResource } from './FeaturesResource';
import { NetworkStateService } from './NetworkStateService';
import { PermissionsService } from './PermissionsService';
import { QuotasService } from './QuotasService';
+import { ServerConfigEventHandler } from './ServerConfigEventHandler';
import { ServerConfigResource } from './ServerConfigResource';
import { ServerService } from './ServerService';
import { ServerSettingsService } from './ServerSettingsService';
import { SessionActionService } from './SessionActionService';
import { SessionDataResource } from './SessionDataResource';
+import { SessionEventSource } from './SessionEventSource';
import { SessionExpireService } from './SessionExpireService';
import { SessionPermissionsResource } from './SessionPermissionsResource';
import { SessionResource } from './SessionResource';
@@ -46,5 +48,7 @@ export const manifest: PluginManifest = {
SessionSettingsService,
WindowEventsService,
QuotasService,
+ ServerConfigEventHandler,
+ SessionEventSource,
],
};
diff --git a/webapp/packages/core-sdk/src/Resource/EventSource.ts b/webapp/packages/core-sdk/src/Resource/EventSource.ts
new file mode 100644
index 0000000000..34f68d7cb2
--- /dev/null
+++ b/webapp/packages/core-sdk/src/Resource/EventSource.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
+
+import type { CachedResource } from './CachedResource';
+
+export type EventSourceCallback = (event: T) => any;
+export interface IEventSource {
+ on(
+ resource: EventSourceCallback,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void;
+}
+
+const DEFAULT_INTERVAL = 1000;
+
+export abstract class EventSource implements IEventSource {
+ onEvent: ISyncExecutor;
+
+ private listening: boolean;
+ private readonly interval: number;
+ constructor(interval?: number) {
+ this.onEvent = new SyncExecutor();
+ this.interval = interval ?? DEFAULT_INTERVAL;
+ this.listening = false;
+ }
+
+ on(
+ resource: EventSourceCallback,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void;
+ on(
+ resource: CachedResource,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void;
+ on(
+ resource: EventSourceCallback | CachedResource,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void {
+ if (typeof resource === 'function') {
+ this.onEvent.addHandler(event => {
+ if (!filter || filter(event)) {
+ resource(mapTo(event));
+ }
+ });
+ } else {
+ this.onEvent.addHandler(event => {
+ if (!filter || filter(event)) {
+ resource.markOutdated(mapTo(event));
+ }
+ });
+ }
+
+ if (!this.listening) {
+ this.listen();
+ }
+ }
+
+ listen() {
+ if (this.listening) {
+ console.warn('Already listening events');
+ return;
+ }
+
+ this.listening = true;
+ let failedRequests = 0;
+ let interval = this.interval;
+
+ const iteration = async () => {
+ try {
+ await this.listener();
+ failedRequests = 0;
+ } catch (exception: any) {
+ console.error(exception);
+ failedRequests++;
+ }
+
+ interval = this.interval * Math.min((failedRequests || 1), 10);
+
+ if (this.listening) {
+ setTimeout(iteration, interval);
+ }
+ };
+
+ iteration();
+ }
+
+ stop() {
+ this.listening = false;
+ }
+
+ event(event: TEvent) {
+ this.onEvent.execute(event);
+ }
+
+ protected abstract listener(): Promise;
+}
\ No newline at end of file
diff --git a/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts b/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts
new file mode 100644
index 0000000000..04d0e6962a
--- /dev/null
+++ b/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
+
+import type { CachedResource } from './CachedResource';
+import type { IEventSource, EventSourceCallback } from './EventSource';
+
+export abstract class ResourceEventHandler implements IEventSource {
+ onEvent: ISyncExecutor;
+
+ private subscribed: boolean;
+ constructor(private readonly source?: IEventSource) {
+ this.onEvent = new SyncExecutor();
+ this.subscribed = false;
+ }
+
+ on(
+ resource: EventSourceCallback,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void;
+ on(
+ resource: CachedResource,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void;
+ on(
+ resource: CachedResource | EventSourceCallback,
+ mapTo: (param: TEvent) => T,
+ filter?: (param: TEvent) => boolean,
+ ): void {
+ if (typeof resource === 'function') {
+ this.onEvent.addHandler(event => {
+ if (!filter || filter(event)) {
+ resource(mapTo(event));
+ }
+ });
+ } else {
+ this.onEvent.addHandler(event => {
+ if (!filter || filter(event)) {
+ resource.markOutdated(mapTo(event));
+ }
+ });
+ }
+
+ if (!this.subscribed) {
+ this.subscribed = true;
+ this.source?.on(this.event.bind(this), this.map.bind(this), this.filter.bind(this));
+ }
+ }
+
+ event(event: TEvent) {
+ this.onEvent.execute(event);
+ }
+
+ abstract map(event: SourceEvent): TEvent;
+ abstract filter(event: SourceEvent): boolean;
+}
\ No newline at end of file
diff --git a/webapp/packages/core-sdk/src/index.ts b/webapp/packages/core-sdk/src/index.ts
index 6326f2d06e..bfbb07a705 100644
--- a/webapp/packages/core-sdk/src/index.ts
+++ b/webapp/packages/core-sdk/src/index.ts
@@ -5,6 +5,8 @@ export * from './Resource/CachedMapResource';
export * from './Resource/CachedResource';
export * from './Resource/CachedResourceIncludes';
export * from './Resource/ResourceKeyList';
+export * from './Resource/EventSource';
+export * from './Resource/ResourceEventHandler';
export * from './CustomGraphQLClient';
export * from './DetailsError';
export * from './EnvironmentService';
diff --git a/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql b/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql
new file mode 100644
index 0000000000..d4a04d5844
--- /dev/null
+++ b/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql
@@ -0,0 +1,10 @@
+query getSessionEvents(
+ $maxEntries: Int!
+) {
+ events: readSessionEvents(
+ maxEntries: $maxEntries
+ ) {
+ eventType
+ eventData
+ }
+}
diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts
index f3967c8b19..22429b1d07 100644
--- a/webapp/packages/core-sdk/src/sdk.ts
+++ b/webapp/packages/core-sdk/src/sdk.ts
@@ -163,6 +163,15 @@ export enum AuthStatus {
Success = 'SUCCESS'
}
+export interface CbEvent {
+ eventData: Scalars['Object'];
+ eventType: CbEventType;
+}
+
+export enum CbEventType {
+ CbConfigChanged = 'cb_config_changed'
+}
+
export interface ConnectionConfig {
authModelId?: InputMaybe;
configurationType?: InputMaybe;
@@ -909,6 +918,7 @@ export interface Query {
navNodeParents: Array;
navRefreshNode?: Maybe;
networkHandlers: Array;
+ readSessionEvents: Array;
readSessionLog: Array;
revokeUserTeam?: Maybe;
rmListProjectGrantedPermissions: Array;
@@ -1151,6 +1161,11 @@ export interface QueryNavRefreshNodeArgs {
}
+export interface QueryReadSessionEventsArgs {
+ maxEntries: Scalars['Int'];
+}
+
+
export interface QueryReadSessionLogArgs {
clearEntries?: InputMaybe;
maxEntries?: InputMaybe;
@@ -2639,6 +2654,13 @@ export type ChangeSessionLanguageMutationVariables = Exact<{
export type ChangeSessionLanguageMutation = { changeSessionLanguage?: boolean };
+export type GetSessionEventsQueryVariables = Exact<{
+ maxEntries: Scalars['Int'];
+}>;
+
+
+export type GetSessionEventsQuery = { events: Array<{ eventType: CbEventType, eventData: any }> };
+
export type OpenSessionMutationVariables = Exact<{
defaultLocale?: InputMaybe;
}>;
@@ -4215,6 +4237,14 @@ export const ChangeSessionLanguageDocument = `
changeSessionLanguage(locale: $locale)
}
`;
+export const GetSessionEventsDocument = `
+ query getSessionEvents($maxEntries: Int!) {
+ events: readSessionEvents(maxEntries: $maxEntries) {
+ eventType
+ eventData
+ }
+}
+ `;
export const OpenSessionDocument = `
mutation openSession($defaultLocale: String) {
session: openSession(defaultLocale: $defaultLocale) {
@@ -4735,6 +4765,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
changeSessionLanguage(variables: ChangeSessionLanguageMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise {
return withWrapper((wrappedRequestHeaders) => client.request(ChangeSessionLanguageDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'changeSessionLanguage', 'mutation');
},
+ getSessionEvents(variables: GetSessionEventsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise {
+ return withWrapper((wrappedRequestHeaders) => client.request(GetSessionEventsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSessionEvents', 'query');
+ },
openSession(variables?: OpenSessionMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise {
return withWrapper((wrappedRequestHeaders) => client.request(OpenSessionDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'openSession', 'mutation');
},