CB-2885-TE-web-session-state-synchronisation-between-several-nodes (#1556)

* CB-3209 Add instance ID to web session (#1532)

* CB-3209 Add instance ID to web session

* CB-3209 Add instance ID to web session

Moves applicationRunId to BaseWebApplication

* CB-3267 Send session expired event on socket create attempt

Implements session restore on socket creation

* CB-3267 Send session expired event on socket create attempt

Changes logging

* CB-3267 Add dialog and notification for node change

* CB-3154 save user creds to secrets in initConnection (#1502)

* CB-3154 save user creds to secrets in initConnection

* CB-3155 add tab 'change credentials'

* CB-2964 feat: lazy loaders

* CB-2964 add reconnect, display tab conditionally

* CB-2964 Init connection on dialog result

* CB-2964 fix logic

* CB-2964 Add logic

* CB-2964 Update ConnectionAuthService.ts

Co-authored-by: Alexey <wrouds@gmail.com>

---------

Co-authored-by: Tatiana Fokina <tatiana.fokina@dbeaver.com>
Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: Aleksey Potsetsuev <wrouds@gmail.com>
Co-authored-by: Tatiana Fokina <123386439+tfokina@users.noreply.github.com>

* CB-3267 Send session expired event on socket create attempt

Implements session restoration on '/sessionState' request

* CB-3267 Send session expired event on socket create attempt

Fixes WebSocketTest.initClient test

* CB-3267 Send session expired event on socket create attempt

Fixes code style issues

* CB-3267 fix localization text

* CB-3267 Send session expired event on socket create attempt

Fixes code style issues

* CB-2885 TE: web session state synchronisation between several nodes

Reverts session restoration on '/sessionState' request

* CB-2885 PR fixes, stop opening websocket when node changes

* CB-2885 TE: web session state synchronisation between several nodes

Replaces Collections.synchronizedList by CopyOnWriteArrayList in CBJettyWebSocketManager#socketBySessionId

* CB-2885 TE: web session state synchronisation between several nodes

Fixes code style issues

* CB-2885 TE: web session state synchronisation between several nodes

Implements restoring old authentication information before creating a new WebSession instance.

* CB-2885 move restore logic to separate method

---------

Co-authored-by: Tatiana Fokina <tatiana.fokina@dbeaver.com>
Co-authored-by: Alexander Skoblikov <aleksandr.skoblikov@dbeaver.com>
Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: Aleksey Potsetsuev <wrouds@gmail.com>
Co-authored-by: Tatiana Fokina <123386439+tfokina@users.noreply.github.com>
This commit is contained in:
Yaroslav Emelyanov
2023-03-30 19:20:43 +03:00
committed by GitHub
co-authored by Alexey Tatiana Fokina dariamarutkina Tatiana Fokina Alexander Skoblikov
parent 2107f4f968
commit 0a5caea6b9
20 changed files with 363 additions and 60 deletions
@@ -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) {
@@ -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();
/**
@@ -125,6 +125,11 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
return id;
}
@NotNull
public WebApplication getApplication() {
return application;
}
@NotNull
@Override
public LocalDateTime getSessionStart() {
@@ -142,10 +142,6 @@ public class WebSession extends BaseWebSession
}
}
public WebApplication getApplication() {
return application;
}
@NotNull
public DBPProject getSingletonProject() {
return getWorkspace().getActiveProject();
@@ -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!
@@ -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
@@ -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<String, List<CBEventsWebSocket>> 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() {
@@ -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<API_TYPE extends DBWService> 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));
@@ -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
@@ -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'],
];
@@ -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', 'Детали ошибки'],
];
@@ -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';
}
}
@@ -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<WsSocketConnectedEvent>(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();
}
}
@@ -54,6 +54,7 @@ implements IServerEventEmitter<ISessionEvent, ISessionEvent, SessionEventId, Ses
private readonly subject: WebSocketSubject<ISessionEvent>;
private readonly oldEventsSubject: Subject<ISessionEvent>;
private readonly retryTimer: Observable<number>;
private disconnected: boolean;
constructor(
private readonly networkStateService: NetworkStateService,
@@ -66,9 +67,10 @@ implements IServerEventEmitter<ISessionEvent, ISessionEvent, SessionEventId, Ses
this.closeSubject = new Subject();
this.openSubject = new Subject();
this.errorSubject = new Subject();
this.disconnected = false;
this.retryTimer = interval(RETRY_INTERVAL)
.pipe(
filter(() => !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<ISessionEvent, ISessionEvent, SessionEventId, Ses
return this;
}
disconnect() {
this.disconnected = true;
}
private handleErrors() {
return (source: Observable<ISessionEvent>):Observable<ISessionEvent> => source.pipe(
share(),
+1
View File
@@ -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';
@@ -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,
+9 -1
View File
@@ -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<CbEventTopic>;
}
export interface WebFeatureSet {
description?: Maybe<Scalars['String']>;
enabled: Scalars['Boolean'];
@@ -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<null, null> = observer(function ServerNodeChangedDialog({
rejectDialog,
}) {
const routerService = useService(RouterService);
const translate = useTranslate();
function reload() {
routerService.reload();
}
return styled(styles)(
<CommonDialogWrapper size='small' fixedSize>
<CommonDialogHeader title="app_root_server_node_changed_title" onReject={rejectDialog} />
<CommonDialogBody noOverflow>
<p>{translate('app_root_server_node_changed_message')}</p>
</CommonDialogBody>
<CommonDialogFooter>
<Button
type="button"
mod={['outlined']}
onClick={rejectDialog}
>
{translate('ui_processing_cancel')}
</Button>
<Button
type="button"
mod={['unelevated']}
onClick={reload}
>
{translate('ui_processing_reload')}
</Button>
</CommonDialogFooter>
</CommonDialogWrapper>
);
});
@@ -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<void> { }
private async handleServerNodeChanged(): Promise<void> {
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 });
}
}
}
@@ -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,