diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 339b7de564..e4ae994d6e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -47,6 +47,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.UUID; /** * Web application @@ -59,6 +60,13 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements private static final Log log = Log.getLog(BaseWebApplication.class); + private final String applicationRunId = UUID.randomUUID().toString(); + + @NotNull + public String getApplicationRunId() { + return applicationRunId; + } + @NotNull @Override public DBPWorkspace createWorkspace(@NotNull DBPPlatform platform, @NotNull IWorkspace eclipseWorkspace) { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index db618b3149..40a8c63b45 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -75,6 +75,12 @@ public interface WebApplication extends DBPApplication { String getApplicationInstanceId() throws DBException; + /** + * Returns random UUID, new on every launch + */ + @NotNull + String getApplicationRunId(); + WSEventController getEventController(); /** diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index 8beb469940..94549fea1d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -125,6 +125,11 @@ public abstract class BaseWebSession extends AbstractSessionPersistent { return id; } + @NotNull + public WebApplication getApplication() { + return application; + } + @NotNull @Override public LocalDateTime getSessionStart() { 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 3ef7c19edb..a4429b1667 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 @@ -142,10 +142,6 @@ public class WebSession extends BaseWebSession } } - public WebApplication getApplication() { - return application; - } - @NotNull public DBPProject getSingletonProject() { return getWorkspace().getActiveProject(); diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index 09acd7d384..59168b6437 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -8,6 +8,8 @@ enum CBServerEventId { cb_session_log_updated, + cb_session_websocket_connected, + cb_datasource_created, cb_datasource_updated, cb_datasource_deleted, @@ -35,6 +37,7 @@ enum CBClientEventId { enum CBEventTopic { cb_config, cb_session_log, + cb_session, cb_datasource, cb_datasource_folder, cb_scripts, @@ -89,6 +92,13 @@ type CBSessionLogEvent implements CBServerEvent { topicId: CBEventTopic } +# WebSocket connected event +type WSSocketConnectedEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + applicationRunId: String! +} + # RM project update event type CBProjectUpdateEvent implements CBServerEvent { id: CBServerEventId! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index d020818140..0caee3e81b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -25,11 +25,11 @@ import org.eclipse.jetty.websocket.api.WebSocketAdapter; import org.eclipse.jetty.websocket.api.WriteCallback; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.WSUtils; import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; import org.jkiss.dbeaver.model.websocket.event.WSClientEventType; import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSSocketConnectedEvent; import org.jkiss.dbeaver.model.websocket.event.client.WSUpdateActiveProjectsClientEvent; import java.io.IOException; @@ -40,19 +40,20 @@ public class CBEventsWebSocket extends WebSocketAdapter implements CBWebSessionE @NotNull private final BaseWebSession webSession; - @NotNull private final WriteCallback callback; public CBEventsWebSocket(@NotNull BaseWebSession webSession) { this.webSession = webSession; - this.callback = new WebSocketPingPongCallback(webSession); + + callback = new WebSocketPingPongCallback(webSession); } @Override public void onWebSocketConnect(Session session) { super.onWebSocketConnect(session); this.webSession.addEventHandler(this); + handeWebSessionEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId())); log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); } @@ -121,7 +122,11 @@ public class CBEventsWebSocket extends WebSocketAdapter implements CBWebSessionE @Override public void close() { - getSession().close(); + var session = getSession(); + // the socket may not be connected to the client + if (session != null) { + getSession().close(); + } } @NotNull diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java index e6e5498497..6d6f498006 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -17,65 +17,78 @@ package io.cloudbeaver.server.websockets; import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebHeadlessSession; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.session.WebSessionManager; import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import javax.servlet.http.HttpServletRequest; public class CBJettyWebSocketManager implements JettyWebSocketCreator { private static final Log log = Log.getLog(CBJettyWebSocketManager.class); private final Map> socketBySessionId = new ConcurrentHashMap<>(); private final WebSessionManager webSessionManager; - public CBJettyWebSocketManager(WebSessionManager webSessionManager) { + public CBJettyWebSocketManager(@NotNull WebSessionManager webSessionManager) { this.webSessionManager = webSessionManager; + new WebSocketPingPongJob(CBPlatform.getInstance(), this).scheduleMonitor(); } + @Nullable @Override - public Object createWebSocket(JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) { - BaseWebSession webSession; + public Object createWebSocket(@NotNull JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) { + var httpRequest = request.getHttpServletRequest(); + var webSession = webSessionManager.getOrRestoreSession(httpRequest); + if (webSession != null) { + // web client session + return createNewEventsWebSocket(webSession); + } + // possible desktop client session try { - webSession = resolveWebSession(request); + var headlessSession = createHeadlessSession(httpRequest); + if (headlessSession == null) { + log.debug("Couldn't create headless session"); + return null; + } + return createNewEventsWebSocket(headlessSession); } catch (DBException e) { log.error("Error resolve websocket session", e); return null; } - if (webSession == null) { - return null; - } - var webSessionId = webSession.getSessionId(); + } + + @NotNull + private CBEventsWebSocket createNewEventsWebSocket(@NotNull BaseWebSession webSession) { + var sessionId = webSession.getSessionId(); var newWebSocket = new CBEventsWebSocket(webSession); - socketBySessionId.computeIfAbsent(webSessionId, key -> new ArrayList<>()).add(newWebSocket); - log.info("Websocket created for session: " + webSessionId); + socketBySessionId.computeIfAbsent(sessionId, key -> new CopyOnWriteArrayList<>()) + .add(newWebSocket); + log.info("Websocket created for session: " + sessionId); return newWebSocket; } @Nullable - private BaseWebSession resolveWebSession(JettyServerUpgradeRequest request) throws DBException { - if (request.getHttpServletRequest().getSession() == null) { + private WebHeadlessSession createHeadlessSession(@NotNull HttpServletRequest request) throws DBException { + var httpSession = request.getSession(false); + if (httpSession == null) { log.debug("CloudBeaver web session not exist, try to create headless session"); - return webSessionManager.getHeadlessSession(request.getHttpServletRequest(), true); + } else { + log.debug("CloudBeaver session not found with id " + httpSession.getId() + ", try to create headless session"); } - var webSessionId = request.getHttpServletRequest().getSession().getId(); - var webSession = webSessionManager.getSession(webSessionId); - if (webSession != null) { - return webSession; - } - log.error("CloudBeaver session not found with id " + webSessionId + ", try to create headless session"); - - return webSessionManager.getHeadlessSession(request.getHttpServletRequest(), true); + return webSessionManager.getHeadlessSession(request, true); } public void sendPing() { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 7a006e1d1b..19b094e3fb 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -28,20 +28,19 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.service.security.SMUtils; 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.rm.RMProject; import org.jkiss.utils.ArrayUtils; -import org.jkiss.utils.CommonUtils; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.*; -import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * Web service implementation @@ -121,6 +120,10 @@ public abstract class WebServiceBindingBase impleme return getWebConnection(getWebSession(env), getProjectReference(env), env.getArgument("connectionId")); } + /** + * Returns WebSession from cache or null + */ + @Nullable public static WebSession findWebSession(DataFetchingEnvironment env) { return CBPlatform.getInstance().getSessionManager().findWebSession( getServletRequest(env)); 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 d2b8887d1f..229f00e67c 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 @@ -37,11 +37,11 @@ import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; +import java.util.*; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import java.util.*; -import java.util.stream.Collectors; /** * Web session manager @@ -62,7 +62,10 @@ public class WebSessionManager { this.application = application; } - public BaseWebSession closeSession(@NotNull HttpServletRequest request) throws DBException { + /** + * Closes Web Session, associated to HttpSession from {@code request} + */ + public BaseWebSession closeSession(@NotNull HttpServletRequest request) { HttpSession session = request.getSession(); if (session != null) { BaseWebSession webSession; @@ -108,24 +111,36 @@ public class WebSessionManager { WebSession webSession; synchronized (sessionMap) { var baseWebSession = sessionMap.get(sessionId); - if (baseWebSession == null) { + if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) { try { webSession = createWebSessionImpl(httpSession); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } sessionMap.put(sessionId, webSession); - - if (!CBApplication.getInstance().isConfigurationMode()) { - if (!httpSession.isNew()) { - webSession.setCacheExpired(true); - if (errorOnNoFound) { - throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); - } - } - tryToRestorePreviousUserSession(webSession); - log.debug("> New web session '" + webSession.getSessionId() + "'"); + } else if (baseWebSession == null) { + try { + webSession = createWebSessionImpl(httpSession); + } catch (DBException e) { + throw new DBWebException("Failed to create web session", e); } + + boolean restored = false; + try { + restored = restorePreviousUserSession(webSession); + } catch (DBException e) { + log.error("Failed to restore previous user session", e); + } + + if (!restored && errorOnNoFound && !httpSession.isNew()) { + throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); + } + + log.debug((restored ? "Restored " : "New ") + "web session '" + webSession.getSessionId() + "'"); + + webSession.setCacheExpired(!httpSession.isNew()); + + sessionMap.put(sessionId, webSession); } else { if (!(baseWebSession instanceof WebSession)) { throw new DBWebException("Unexpected session type: " + baseWebSession.getClass().getName()); @@ -144,17 +159,67 @@ public class WebSessionManager { return webSession; } - private void tryToRestorePreviousUserSession(WebSession webSession) { - try { - SMAuthInfo oldAuthInfo = webSession.getSecurityController().restoreUserSession(webSession.getSessionId()); - if (oldAuthInfo == null) { - return; - } - boolean linkWithActiveUser = false; // because it's old credentials and should already be linked if needed - new WebSessionAuthProcessor(webSession, oldAuthInfo, linkWithActiveUser).authenticateSession(); - } catch (DBException e) { - log.error("Failed to restore previous user session", e); + /** + * Returns not expired session from cache, or restore it. + * + * @return WebSession object or null, if session expired or invalid + */ + @Nullable + public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) { + var httpSession = request.getSession(); + if (httpSession == null) { + log.debug("Http session is null. No Web Session returned"); + return null; } + var sessionId = httpSession.getId(); + WebSession webSession; + synchronized (sessionMap) { + if (sessionMap.containsKey(sessionId)) { + var cachedWebSession = sessionMap.get(sessionId); + if (!(cachedWebSession instanceof WebSession)) { + log.warn("Unexpected session type: " + cachedWebSession.getClass().getName()); + return null; + } + return (WebSession) cachedWebSession; + } else { + try { + var oldAuthInfo = getApplication().getSecurityController().restoreUserSession(sessionId); + if (oldAuthInfo == null) { + log.debug("Couldn't restore previous user session '" + sessionId + "'"); + return null; + } + + webSession = createWebSessionImpl(httpSession); + restorePreviousUserSession(webSession, oldAuthInfo); + + sessionMap.put(sessionId, webSession); + log.debug("Web session restored"); + return webSession; + } catch (DBException e) { + log.error("Failed to restore previous user session", e); + return null; + } + } + } + } + + private boolean restorePreviousUserSession(@NotNull WebSession webSession) throws DBException { + var oldAuthInfo = webSession.getSecurityController().restoreUserSession(webSession.getSessionId()); + if (oldAuthInfo == null) { + return false; + } + + restorePreviousUserSession(webSession, oldAuthInfo); + return true; + } + + private void restorePreviousUserSession( + @NotNull WebSession webSession, + @NotNull SMAuthInfo authInfo + ) throws DBException { + var linkWithActiveUser = false; // because its old credentials and should already be linked if needed + new WebSessionAuthProcessor(webSession, authInfo, linkWithActiveUser) + .authenticateSession(); } @NotNull diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index c8d6b76ad7..f6f8de7995 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -10,6 +10,7 @@ export default [ ['ui_processing_loading', 'Loading...'], ['ui_processing_cancel', 'Cancel'], ['ui_processing_canceling', 'Cancelling...'], + ['ui_processing_reload', 'Reload'], ['ui_processing_retry', 'Retry'], ['ui_processing_ok', 'Ok'], ['ui_processing_create', 'Create'], @@ -101,6 +102,8 @@ export default [ ['app_root_session_expired_title', 'Session expired'], ['app_root_session_expired_message', 'Session has expired. Do you want to reload?'], ['app_root_session_expired_reload', 'Reload'], + ['app_root_server_node_changed_title', 'Server node changed'], + ['app_root_server_node_changed_message', 'Server node changed. You need to reload the application.'], ['app_root_quota_exceeded', 'Quota exceeded'], ['core_eventsLog_dbeaverErrorDetails', 'Error details'], ]; diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 776e2b71ea..9f7df7bd7a 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -8,6 +8,7 @@ export default [ ['ui_processing_loading', 'Загрузка...'], ['ui_processing_cancel', 'Отменить'], ['ui_processing_canceling', 'Отмена...'], + ['ui_processing_reload', 'Перезагрузить'], ['ui_processing_retry', 'Повторить'], ['ui_processing_ok', 'Принять'], ['ui_processing_create', 'Создать'], @@ -99,6 +100,8 @@ export default [ ['app_root_session_expired_title', 'Сессия истекла'], ['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'], ['app_root_session_expired_reload', 'Перезагрузить'], + ['app_root_server_node_changed_title', 'Узел сервера изменён'], + ['app_root_server_node_changed_message', 'Узел сервера изменён. Необходимо перезагрузить приложение.'], ['app_root_quota_exceeded', 'Квота превышена'], ['core_eventsLog_dbeaverErrorDetails', 'Детали ошибки'], ]; diff --git a/webapp/packages/core-root/src/ServerNodeError.ts b/webapp/packages/core-root/src/ServerNodeError.ts new file mode 100644 index 0000000000..706a84b7db --- /dev/null +++ b/webapp/packages/core-root/src/ServerNodeError.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export class ServerNodeError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'Server Node Error'; + } +} \ No newline at end of file diff --git a/webapp/packages/core-root/src/ServerNodeService.ts b/webapp/packages/core-root/src/ServerNodeService.ts new file mode 100644 index 0000000000..ba84463398 --- /dev/null +++ b/webapp/packages/core-root/src/ServerNodeService.ts @@ -0,0 +1,47 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { Dependency, injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { GraphQLService, WsSocketConnectedEvent } from '@cloudbeaver/core-sdk'; + +import { ServerNodeError } from './ServerNodeError'; +import { ServerEventId, SessionEventSource } from './SessionEventSource'; + +@injectable() +export class ServerNodeService extends Dependency { + private applicationRunId: string | null; + onApplicationRunIdChange: IExecutor; + constructor( + private readonly graphQLService: GraphQLService, + private readonly sessionEventSource: SessionEventSource + ) { + super(); + this.onApplicationRunIdChange = new Executor(); + this.applicationRunId = null; + this.sessionEventSource.onEvent(ServerEventId.CbSessionWebsocketConnected, data => { + this.applicationRunIdChanged(data.applicationRunId); + }); + } + + private applicationRunIdChanged(applicationRunId: string): void { + if (this.applicationRunId === null) { + this.applicationRunId = applicationRunId; + return; + } + + if (this.applicationRunId === applicationRunId) { + return; + } + + this.applicationRunId = applicationRunId; + this.graphQLService.blockRequests(new ServerNodeError('Server node changed')); + this.sessionEventSource.disconnect(); + this.onApplicationRunIdChange.execute(); + } +} diff --git a/webapp/packages/core-root/src/SessionEventSource.ts b/webapp/packages/core-root/src/SessionEventSource.ts index 1558ed09e8..63845bede9 100644 --- a/webapp/packages/core-root/src/SessionEventSource.ts +++ b/webapp/packages/core-root/src/SessionEventSource.ts @@ -54,6 +54,7 @@ implements IServerEventEmitter; private readonly oldEventsSubject: Subject; private readonly retryTimer: Observable; + private disconnected: boolean; constructor( private readonly networkStateService: NetworkStateService, @@ -66,9 +67,10 @@ implements IServerEventEmitter !this.sessionExpireService.expired && networkStateService.state) + filter(() => !this.sessionExpireService.expired && networkStateService.state && !this.disconnected) ); this.subject = webSocket({ url: environmentService.wsEndpoint, @@ -154,6 +156,10 @@ implements IServerEventEmitter):Observable => source.pipe( share(), diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index ee81d0218a..e2a30cd2f6 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -21,5 +21,6 @@ export * from './SessionActionService'; export * from './SessionDataResource'; export * from './SessionSettingsService'; export * from './SessionExpireService'; +export * from './ServerNodeService'; export * from './WindowEventsService'; export { manifest as coreRootManifest } from './manifest'; diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts index 9e6a8a45e8..9128be618f 100644 --- a/webapp/packages/core-root/src/manifest.ts +++ b/webapp/packages/core-root/src/manifest.ts @@ -15,6 +15,7 @@ import { PermissionsService } from './PermissionsService'; import { QuotasService } from './QuotasService'; import { ServerConfigEventHandler } from './ServerConfigEventHandler'; import { ServerConfigResource } from './ServerConfigResource'; +import { ServerNodeService } from './ServerNodeService'; import { ServerService } from './ServerService'; import { ServerSettingsService } from './ServerSettingsService'; import { SessionActionService } from './SessionActionService'; @@ -44,6 +45,7 @@ export const manifest: PluginManifest = { SessionActionService, SessionDataResource, SessionExpireService, + ServerNodeService, SessionResource, SessionService, SessionSettingsService, diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 70cedb7904..d392363af5 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -202,6 +202,7 @@ export enum CbEventTopic { CbDatasourceFolder = 'cb_datasource_folder', CbProjects = 'cb_projects', CbScripts = 'cb_scripts', + CbSession = 'cb_session', CbSessionLog = 'cb_session_log' } @@ -248,7 +249,8 @@ export enum CbServerEventId { CbRmResourceCreated = 'cb_rm_resource_created', CbRmResourceDeleted = 'cb_rm_resource_deleted', CbRmResourceUpdated = 'cb_rm_resource_updated', - CbSessionLogUpdated = 'cb_session_log_updated' + CbSessionLogUpdated = 'cb_session_log_updated', + CbSessionWebsocketConnected = 'cb_session_websocket_connected' } export interface CbSessionLogEvent extends CbServerEvent { @@ -1730,6 +1732,12 @@ export interface UserInfo { userId: Scalars['ID']; } +export interface WsSocketConnectedEvent extends CbServerEvent { + applicationRunId: Scalars['String']; + id: CbServerEventId; + topicId?: Maybe; +} + export interface WebFeatureSet { description?: Maybe; enabled: Scalars['Boolean']; diff --git a/webapp/packages/plugin-root/src/ServerNodeChangedDialog/ServerNodeChangedDialog.tsx b/webapp/packages/plugin-root/src/ServerNodeChangedDialog/ServerNodeChangedDialog.tsx new file mode 100644 index 0000000000..0b329e38c1 --- /dev/null +++ b/webapp/packages/plugin-root/src/ServerNodeChangedDialog/ServerNodeChangedDialog.tsx @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import styled, { css } from 'reshadow'; + +import { Button, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs'; +import { RouterService } from '@cloudbeaver/core-routing'; + +const styles = css` + p { + margin: 0; + } + CommonDialogFooter { + align-items: center; + justify-content: flex-end; + gap: 24px; + } +`; + +export const ServerNodeChangedDialog: DialogComponent = observer(function ServerNodeChangedDialog({ + rejectDialog, +}) { + const routerService = useService(RouterService); + const translate = useTranslate(); + function reload() { + routerService.reload(); + } + + return styled(styles)( + + + +

{translate('app_root_server_node_changed_message')}

+
+ + + + +
+ ); +}); diff --git a/webapp/packages/plugin-root/src/ServerNodeChangedDialog/ServerNodeChangedDialogService.ts b/webapp/packages/plugin-root/src/ServerNodeChangedDialog/ServerNodeChangedDialogService.ts new file mode 100644 index 0000000000..ebb212568e --- /dev/null +++ b/webapp/packages/plugin-root/src/ServerNodeChangedDialog/ServerNodeChangedDialogService.ts @@ -0,0 +1,45 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { ActionSnackbar } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; +import { ServerNodeService } from '@cloudbeaver/core-root'; +import { RouterService } from '@cloudbeaver/core-routing'; + +import { ServerNodeChangedDialog } from './ServerNodeChangedDialog'; + +@injectable() +export class ServerNodeChangedDialogService extends Bootstrap { + constructor( + private readonly routerService: RouterService, + private readonly notificationService: NotificationService, + private readonly commonDialogService: CommonDialogService, + private readonly serverNodeService: ServerNodeService + ) { + super(); + } + + register(): void { + this.serverNodeService.onApplicationRunIdChange.addPostHandler(this.handleServerNodeChanged.bind(this)); + } + + load(): void | Promise { } + + private async handleServerNodeChanged(): Promise { + const state = await this.commonDialogService.open(ServerNodeChangedDialog, null); + + if (state === DialogueStateResult.Rejected) { + this.notificationService.customNotification(() => ActionSnackbar, { + actionText: 'ui_processing_reload', + onAction: () => this.routerService.reload(), + }, { title: 'app_root_server_node_changed_title', message: 'app_root_server_node_changed_message', persistent: true, type: ENotificationType.Error }); + } + } +} diff --git a/webapp/packages/plugin-root/src/manifest.ts b/webapp/packages/plugin-root/src/manifest.ts index 81b5d2094b..4126110415 100644 --- a/webapp/packages/plugin-root/src/manifest.ts +++ b/webapp/packages/plugin-root/src/manifest.ts @@ -12,6 +12,7 @@ import { DataSynchronizationResolverBootstrap } from './DataSynchronization/Data import { LocaleService } from './LocaleService'; import { NetworkStateNotificationService } from './NetworkStateNotification/NetworkStateNotificationService'; import { PluginBootstrap } from './PluginBootstrap'; +import { ServerNodeChangedDialogService } from './ServerNodeChangedDialog/ServerNodeChangedDialogService'; import { SessionExpiredDialogService } from './SessionExpireDialog/SessionExpiredDialogService'; import { SessionExpireWarningDialogService } from './SessionExpireWarningDialog/SessionExpireWarningDialogService'; @@ -21,6 +22,7 @@ export const rootPlugin: PluginManifest = { LocaleService, PluginBootstrap, SessionExpiredDialogService, + ServerNodeChangedDialogService, SessionExpireWarningDialogService, NetworkStateNotificationService, DataSynchronizationResolverBootstrap,