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 73f1f0b61b..eee3bae054 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 @@ -521,6 +521,7 @@ public synchronized void updateInfo( HttpServletRequest request, HttpServletResponse response ) throws DBWebException { + log.debug("Update session lifetime " + getSessionId() + " for user " + getUserId()); touchSession(); HttpSession httpSession = request.getSession(); this.lastRemoteAddr = request.getRemoteAddr(); diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index e0b7e6ae7b..b393c48824 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -566,7 +566,9 @@ extend type Mutation { closeSession: Boolean # Refreshes session on server and returns its state - touchSession: Boolean + touchSession: Boolean @deprecated(reason: "use updateSession instead") + # Refreshes session on server and returns session state + updateSession: SessionInfo! @since(version: "24.0.0") # Refresh session connection list refreshSessionConnections: Boolean 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 65007f34b9..e684f44669 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 @@ -90,9 +90,14 @@ WebSession openSession( @WebAction(authRequired = false) boolean closeSession(HttpServletRequest request) throws DBWebException; + @Deprecated @WebAction(authRequired = false) boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse servletResponse) throws DBWebException; + @WebAction(authRequired = false) + WebSession updateSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) + throws DBWebException; + @WebAction(authRequired = false) boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) 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 76f2e718cd..7c55eb2c37 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 @@ -99,6 +99,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { .dataFetcher("closeSession", env -> getService(env).closeSession(GraphQLEndpoint.getServletRequest(env))) .dataFetcher("touchSession", env -> getService(env).touchSession( GraphQLEndpoint.getServletRequest(env), GraphQLEndpoint.getServletResponse(env))) + .dataFetcher("updateSession", env -> getService(env).updateSession( + GraphQLEndpoint.getServletRequest(env), GraphQLEndpoint.getServletResponse(env))) .dataFetcher("refreshSessionConnections", env -> getService(env).refreshSessionConnections( GraphQLEndpoint.getServletRequest(env), GraphQLEndpoint.getServletResponse(env))) .dataFetcher("changeSessionLanguage", env -> getService(env).changeSessionLanguage(getWebSession(env), env.getArgument("locale"))) 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 5707ac37ca..ad7eec7b06 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 @@ -29,6 +29,7 @@ import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.core.DBWServiceCore; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.service.session.WebSessionManager; import io.cloudbeaver.utils.WebConnectionFolderUtils; import io.cloudbeaver.utils.WebDataSourceUtils; import io.cloudbeaver.utils.WebEventUtils; @@ -285,6 +286,14 @@ public boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpSe return CBPlatform.getInstance().getSessionManager().touchSession(request, response); } + @Override + public WebSession updateSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) + throws DBWebException { + WebSessionManager sessionManager = CBPlatform.getInstance().getSessionManager(); + sessionManager.touchSession(request, response); + return sessionManager.getWebSession(request, response, true); + } + @Override public boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { WebSession session = CBPlatform.getInstance().getSessionManager().getWebSession(request, response); 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 c481e00fd7..295a0c5734 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 @@ -93,12 +93,11 @@ public WebSession getWebSession(@NotNull HttpServletRequest request, } @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, boolean errorOnNoFound) throws DBWebException { - return getWebSession(request, response, true, errorOnNoFound); - } - - @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, boolean updateInfo, boolean errorOnNoFound) throws DBWebException { + public WebSession getWebSession( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + boolean errorOnNoFound + ) throws DBWebException { HttpSession httpSession = request.getSession(true); String sessionId = httpSession.getId(); WebSession webSession; @@ -132,6 +131,7 @@ public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull Ht log.debug((restored ? "Restored " : "New ") + "web session '" + webSession.getSessionId() + "'"); webSession.setCacheExpired(!httpSession.isNew()); + webSession.updateInfo(request, response); sessionMap.put(sessionId, webSession); } else { @@ -139,13 +139,6 @@ public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull Ht throw new DBWebException("Unexpected session type: " + baseWebSession.getClass().getName()); } webSession = (WebSession) baseWebSession; - if (updateInfo) { - // Update only once per request - if (!CommonUtils.toBoolean(request.getAttribute("sessionUpdated"))) { - webSession.updateInfo(request, response); - request.setAttribute("sessionUpdated", true); - } - } } } @@ -328,16 +321,21 @@ public WebHeadlessSession getHeadlessSession(HttpServletRequest request, boolean */ public void sendSessionsStates() { synchronized (sessionMap) { - for (var session : sessionMap.values()) { - if (session instanceof WebHeadlessSession) { - continue; - } - try { - session.addSessionEvent(new WSSessionStateEvent(session.getRemainingTime(), session.isValid())); - } catch (Exception e) { - log.error("Failed to refresh session state: " + session.getSessionId(), e); - } - } + sessionMap.values() + .parallelStream() + .filter(session -> { + if (session instanceof WebSession webSession) { + return webSession.isAuthorizedInSecurityManager(); + } + return false; + }) + .forEach(session -> { + try { + session.addSessionEvent(new WSSessionStateEvent(session.getRemainingTime(), session.isValid())); + } catch (Exception e) { + log.error("Failed to refresh session state: " + session.getSessionId(), e); + } + }); } } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index af94e9a4e9..69c203f7c8 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -33,6 +33,7 @@ import io.cloudbeaver.service.DBWServiceServerConfigurator; import io.cloudbeaver.service.admin.*; import io.cloudbeaver.service.security.SMUtils; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -257,7 +258,9 @@ public boolean grantUserTeam(@NotNull WebSession webSession, String user, String if (grantor == null) { throw new DBWebException("Cannot grant team in anonymous mode"); } - if (CommonUtils.equalObjects(user, webSession.getUser().getUserId())) { + if (!WebAppUtils.getWebApplication().isDistributed() + && CommonUtils.equalObjects(user, webSession.getUser().getUserId()) + ) { throw new DBWebException("You cannot edit your own permissions"); } try { @@ -281,7 +284,9 @@ public boolean revokeUserTeam(@NotNull WebSession webSession, String user, Strin if (grantor == null) { throw new DBWebException("Cannot revoke team in anonymous mode"); } - if (CommonUtils.equalObjects(user, webSession.getUser().getUserId())) { + if (!WebAppUtils.getWebApplication().isDistributed() && + CommonUtils.equalObjects(user, webSession.getUser().getUserId()) + ) { throw new DBWebException("You cannot edit your own permissions"); } try { diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java index c1dcf52b53..073cbc38a8 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferServlet.java @@ -34,6 +34,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; public class WebDataTransferServlet extends WebServiceServletBase { @@ -67,7 +68,7 @@ protected void processServiceRequest(WebSession session, HttpServletRequest requ } String fileName = taskInfo.getExportFileName(); if (!CommonUtils.isEmpty(fileName)) { - fileName += "." + WebDataTransferUtils.getProcessorFileExtension(processor); + fileName += "." + WebDataTransferUtils.getProcessorFileExtension(processor, taskInfo.getParameters().getProcessorProperties()); } else { fileName = taskInfo.getDataFileId(); } diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java index b0ac4d45fd..d05129736a 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebDataTransferUtils.java @@ -23,9 +23,12 @@ import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor; import org.jkiss.utils.CommonUtils; +import java.util.Map; + class WebDataTransferUtils { private static final Log log = Log.getLog(WebDataTransferUtils.class); + public static final String EXTENSION = "extension"; public static String getProcessorFileExtension(DataTransferProcessorDescriptor processor) { @@ -34,6 +37,14 @@ public static String getProcessorFileExtension(DataTransferProcessorDescriptor p return CommonUtils.isEmpty(ext) ? "data" : ext; } + public static String getProcessorFileExtension(DataTransferProcessorDescriptor processor, Map processorProperties) { + if (processorProperties != null && processorProperties.get(EXTENSION) != null) { + return CommonUtils.toString(processorProperties.get(EXTENSION), "data"); + } + + return getProcessorFileExtension(processor); + } + public static String normalizeFileName( @NotNull String fileName, @NotNull WebDataTransferOutputSettings outputSettings diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java index f503fc3dfb..d76d32034f 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java @@ -107,7 +107,15 @@ public WebAsyncTaskInfo dataTransferExportDataFromContainer( } @NotNull - private String makeUniqueFileName(WebSQLProcessor sqlProcessor, DataTransferProcessorDescriptor processor) { + private String makeUniqueFileName( + WebSQLProcessor sqlProcessor, + DataTransferProcessorDescriptor processor, + Map processorProperties + ) { + if (processorProperties != null && processorProperties.get(StreamConsumerSettings.PROP_FILE_EXTENSION) != null) { + return sqlProcessor.getWebSession().getSessionId() + "_" + UUID.randomUUID() + + "." + processorProperties.get(StreamConsumerSettings.PROP_FILE_EXTENSION); + } return sqlProcessor.getWebSession().getSessionId() + "_" + UUID.randomUUID() + "." + WebDataTransferUtils.getProcessorFileExtension(processor); } @@ -157,7 +165,8 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { monitor.beginTask("Export data", 1); try { monitor.subTask("Export data using " + processor.getName()); - Path exportFile = dataExportFolder.resolve(makeUniqueFileName(sqlProcessor, processor)); + Path exportFile = dataExportFolder.resolve( + makeUniqueFileName(sqlProcessor, processor, parameters.getProcessorProperties())); try { exportData(monitor, processor, dataContainer, parameters, resultsInfo, exportFile); } catch (Exception e) { diff --git a/webapp/packages/core-administration/package.json b/webapp/packages/core-administration/package.json index a8208e3e31..ab3efce199 100644 --- a/webapp/packages/core-administration/package.json +++ b/webapp/packages/core-administration/package.json @@ -36,6 +36,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/core-administration/src/AdministrationSettingsService.test.ts b/webapp/packages/core-administration/src/AdministrationSettingsService.test.ts index e01defca4a..6fadda2735 100644 --- a/webapp/packages/core-administration/src/AdministrationSettingsService.test.ts +++ b/webapp/packages/core-administration/src/AdministrationSettingsService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -37,6 +38,7 @@ const app = createApp( // coreRoutingManifest, // coreThemingManifest, coreLocalizationManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/core-administration/tsconfig.json b/webapp/packages/core-administration/tsconfig.json index b71c4bdc46..3a930b8e3a 100644 --- a/webapp/packages/core-administration/tsconfig.json +++ b/webapp/packages/core-administration/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-data-context/tsconfig.json" }, diff --git a/webapp/packages/core-app/package.json b/webapp/packages/core-app/package.json index cd38d98b18..562afd0017 100644 --- a/webapp/packages/core-app/package.json +++ b/webapp/packages/core-app/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@cloudbeaver/core-blocks": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", diff --git a/webapp/packages/core-app/src/Body.tsx b/webapp/packages/core-app/src/Body.tsx index 6f7849ad28..7de2d8538f 100644 --- a/webapp/packages/core-app/src/Body.tsx +++ b/webapp/packages/core-app/src/Body.tsx @@ -21,6 +21,7 @@ import { useAppVersion } from '@cloudbeaver/core-version'; import style from './Body.m.css'; import { useAppHeight } from './useAppHeight'; +import { useClientActivity } from './useClientActivity'; export const Body = observer(function Body() { // const serverConfigLoader = useResource(Body, ServerConfigResource, undefined); @@ -44,6 +45,7 @@ export const Body = observer(function Body() { }); useAppHeight(); + useClientActivity(); return ( diff --git a/webapp/packages/core-app/src/useClientActivity.ts b/webapp/packages/core-app/src/useClientActivity.ts new file mode 100644 index 0000000000..55ac97865e --- /dev/null +++ b/webapp/packages/core-app/src/useClientActivity.ts @@ -0,0 +1,46 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { useEffect } from 'react'; + +import { ClientActivityService } from '@cloudbeaver/core-client-activity'; +import { useService } from '@cloudbeaver/core-di'; +import { throttle } from '@cloudbeaver/core-utils'; + +const UPDATE_THROTTLE = 300; + +export function useClientActivity() { + const clientActivityService = useService(ClientActivityService); + + const updateActivity = throttle(function updateActivity() { + clientActivityService.updateActivity(); + }, UPDATE_THROTTLE); + + function subscribeEvents() { + document.addEventListener('mousemove', updateActivity); + document.addEventListener('click', updateActivity); + document.addEventListener('keydown', updateActivity); + document.addEventListener('scroll', updateActivity); + document.addEventListener('touchstart', updateActivity); + } + + function unsubscribeEvents() { + document.removeEventListener('mousemove', updateActivity); + document.removeEventListener('click', updateActivity); + document.removeEventListener('keydown', updateActivity); + document.removeEventListener('scroll', updateActivity); + document.removeEventListener('touchstart', updateActivity); + } + + useEffect(() => { + subscribeEvents(); + + return () => { + unsubscribeEvents(); + }; + }, []); +} diff --git a/webapp/packages/core-app/tsconfig.json b/webapp/packages/core-app/tsconfig.json index a8bdc73634..0f575e5654 100644 --- a/webapp/packages/core-app/tsconfig.json +++ b/webapp/packages/core-app/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../core-blocks/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/core-authentication/package.json b/webapp/packages/core-authentication/package.json index 0779571fd9..b192ebc797 100644 --- a/webapp/packages/core-authentication/package.json +++ b/webapp/packages/core-authentication/package.json @@ -35,6 +35,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/core-authentication/src/AuthSettingsService.test.ts b/webapp/packages/core-authentication/src/AuthSettingsService.test.ts index a726605a30..2a8f059912 100644 --- a/webapp/packages/core-authentication/src/AuthSettingsService.test.ts +++ b/webapp/packages/core-authentication/src/AuthSettingsService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -40,6 +41,7 @@ const app = createApp( coreRoutingManifest, coreThemingManifest, coreLocalizationManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/core-authentication/tsconfig.json b/webapp/packages/core-authentication/tsconfig.json index 5b1b6352b5..9b1bd78411 100644 --- a/webapp/packages/core-authentication/tsconfig.json +++ b/webapp/packages/core-authentication/tsconfig.json @@ -14,6 +14,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-data-context/tsconfig.json" }, diff --git a/webapp/packages/core-blocks/package.json b/webapp/packages/core-blocks/package.json index 21b5999970..734f2e38a4 100644 --- a/webapp/packages/core-blocks/package.json +++ b/webapp/packages/core-blocks/package.json @@ -40,6 +40,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/core-blocks/src/ErrorMessage.test.tsx b/webapp/packages/core-blocks/src/ErrorMessage.test.tsx index 3fdafe2caf..c6590c4480 100644 --- a/webapp/packages/core-blocks/src/ErrorMessage.test.tsx +++ b/webapp/packages/core-blocks/src/ErrorMessage.test.tsx @@ -9,6 +9,7 @@ import '@testing-library/jest-dom'; import { screen } from '@testing-library/react'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -35,6 +36,7 @@ const app = createApp( coreBrowserManifest, coreThemingManifest, coreLocalizationManifest, + coreClientActivityManifest, ); mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/core-blocks/src/FormControls/Filter.tsx b/webapp/packages/core-blocks/src/FormControls/Filter.tsx index 1b7486377c..735f9fc2f1 100644 --- a/webapp/packages/core-blocks/src/FormControls/Filter.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Filter.tsx @@ -103,6 +103,7 @@ export const Filter = observer>(functio
= function ItemListSearch({ value, boolean; geLayoutSize?: (property: ObjectPropertyInfo) => ILayoutSizeProps; onFocus?: (name: string) => void; @@ -44,6 +45,7 @@ export const ObjectPropertyInfoForm = observer(function state, defaultState, category, + disableAutoCompleteForPasswords = false, editable = true, className, autofillToken = '', @@ -90,7 +92,7 @@ export const ObjectPropertyInfoForm = observer(function state={state} defaultState={defaultState} editable={editable} - autofillToken={autofillToken} + autofillToken={property.features.includes('password') && disableAutoCompleteForPasswords ? 'new-password' : autofillToken} disabled={disabled} readOnly={readOnly} autoHide={autoHide} diff --git a/webapp/packages/core-blocks/tsconfig.json b/webapp/packages/core-blocks/tsconfig.json index 337afba540..44632bf437 100644 --- a/webapp/packages/core-blocks/tsconfig.json +++ b/webapp/packages/core-blocks/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/core-bootstrap/package.json b/webapp/packages/core-bootstrap/package.json index cd4d661c91..e58afffc61 100644 --- a/webapp/packages/core-bootstrap/package.json +++ b/webapp/packages/core-bootstrap/package.json @@ -24,6 +24,7 @@ "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-browser-cookies": "~0.1.0", "@cloudbeaver/core-browser-settings": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 1c9e7567cd..51fb31ba3b 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -12,6 +12,7 @@ import { coreBlocksManifest } from '@cloudbeaver/core-blocks'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreBrowserCookiesManifest } from '@cloudbeaver/core-browser-cookies'; import { coreBrowserSettingsManifest } from '@cloudbeaver/core-browser-settings'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; import { coreDIManifest, PluginManifest } from '@cloudbeaver/core-di'; import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; @@ -74,5 +75,6 @@ export const coreManifests: PluginManifest[] = [ coreDialogsManifest, resourceManagerManifest, coreAppManifest, + coreClientActivityManifest, coreNavigationTree, ]; diff --git a/webapp/packages/core-bootstrap/tsconfig.json b/webapp/packages/core-bootstrap/tsconfig.json index 26e6ec4dc5..fdb992e6d4 100644 --- a/webapp/packages/core-bootstrap/tsconfig.json +++ b/webapp/packages/core-bootstrap/tsconfig.json @@ -27,6 +27,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-connections/tsconfig.json" }, diff --git a/webapp/packages/core-browser-settings/package.json b/webapp/packages/core-browser-settings/package.json index 7fc54220d5..d0fddf81ef 100644 --- a/webapp/packages/core-browser-settings/package.json +++ b/webapp/packages/core-browser-settings/package.json @@ -27,6 +27,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-product": "~0.1.0", diff --git a/webapp/packages/core-browser-settings/src/BrowserSettingsService.test.ts b/webapp/packages/core-browser-settings/src/BrowserSettingsService.test.ts index d8fcf540f3..2049e9aff6 100644 --- a/webapp/packages/core-browser-settings/src/BrowserSettingsService.test.ts +++ b/webapp/packages/core-browser-settings/src/BrowserSettingsService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; import { coreProductManifest } from '@cloudbeaver/core-product'; @@ -33,6 +34,7 @@ const app = createApp( coreSDKManifest, coreSettingsManifest, coreLocalizationManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/core-browser-settings/tsconfig.json b/webapp/packages/core-browser-settings/tsconfig.json index 4e8b9c2fa6..dec9a7e73a 100644 --- a/webapp/packages/core-browser-settings/tsconfig.json +++ b/webapp/packages/core-browser-settings/tsconfig.json @@ -14,6 +14,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/core-client-activity/.gitignore b/webapp/packages/core-client-activity/.gitignore new file mode 100644 index 0000000000..99902db863 --- /dev/null +++ b/webapp/packages/core-client-activity/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/core-client-activity/package.json b/webapp/packages/core-client-activity/package.json new file mode 100644 index 0000000000..8ac8be39f0 --- /dev/null +++ b/webapp/packages/core-client-activity/package.json @@ -0,0 +1,28 @@ +{ + "name": "@cloudbeaver/core-client-activity", + "sideEffects": [ + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc -b", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "validate-dependencies": "core-cli-validate-dependencies", + "update-ts-references": "rimraf --glob dist && typescript-resolve-references" + }, + "dependencies": { + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-executor": "~0.1.0", + "mobx": "^6.12.0" + }, + "peerDependencies": {}, + "devDependencies": { + "typescript": "^5.3.2" + } +} diff --git a/webapp/packages/core-client-activity/src/ClientActivityService.ts b/webapp/packages/core-client-activity/src/ClientActivityService.ts new file mode 100644 index 0000000000..73d71506b0 --- /dev/null +++ b/webapp/packages/core-client-activity/src/ClientActivityService.ts @@ -0,0 +1,58 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; + +const INACTIVE_PERIOD_TIME = 1000 * 60; + +@injectable() +export class ClientActivityService { + private timer: ReturnType | null; + public isActive: boolean; + public onActiveStateChange: IExecutor; + + constructor() { + this.setActivity = this.setActivity.bind(this); + this.updateActivity = this.updateActivity.bind(this); + this.resetActivity = this.resetActivity.bind(this); + + this.timer = null; + this.isActive = false; + this.onActiveStateChange = new Executor(); + + makeObservable(this, { + isActive: observable, + }); + } + + private setActivity(value: boolean) { + this.isActive = value; + this.onActiveStateChange.execute(value); + } + + resetActivity() { + this.setActivity(false); + + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + updateActivity() { + this.setActivity(true); + + if (this.timer) { + clearTimeout(this.timer); + } + + this.timer = setTimeout(this.resetActivity, INACTIVE_PERIOD_TIME); + } +} diff --git a/webapp/packages/core-client-activity/src/index.ts b/webapp/packages/core-client-activity/src/index.ts new file mode 100644 index 0000000000..b06c56627e --- /dev/null +++ b/webapp/packages/core-client-activity/src/index.ts @@ -0,0 +1,2 @@ +export * from './ClientActivityService'; +export * from './manifest'; diff --git a/webapp/packages/core-client-activity/src/manifest.ts b/webapp/packages/core-client-activity/src/manifest.ts new file mode 100644 index 0000000000..469b923fc3 --- /dev/null +++ b/webapp/packages/core-client-activity/src/manifest.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 type { PluginManifest } from '@cloudbeaver/core-di'; + +import { ClientActivityService } from './ClientActivityService'; + +export const coreClientActivityManifest: PluginManifest = { + info: { + name: 'Core Client Activity', + }, + + providers: [ClientActivityService], +}; diff --git a/webapp/packages/core-client-activity/tsconfig.json b/webapp/packages/core-client-activity/tsconfig.json new file mode 100644 index 0000000000..df9dba157c --- /dev/null +++ b/webapp/packages/core-client-activity/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + { + "path": "../core-di/tsconfig.json" + }, + { + "path": "../core-executor/tsconfig.json" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*", + "dist/**/*" + ] +} diff --git a/webapp/packages/core-events/package.json b/webapp/packages/core-events/package.json index ca4b248483..4573e974a6 100644 --- a/webapp/packages/core-events/package.json +++ b/webapp/packages/core-events/package.json @@ -29,6 +29,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-product": "~0.1.0", diff --git a/webapp/packages/core-events/src/EventsSettingsService.test.ts b/webapp/packages/core-events/src/EventsSettingsService.test.ts index 6b5b9237ef..667d98c0c1 100644 --- a/webapp/packages/core-events/src/EventsSettingsService.test.ts +++ b/webapp/packages/core-events/src/EventsSettingsService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; import { coreProductManifest } from '@cloudbeaver/core-product'; @@ -33,6 +34,7 @@ const app = createApp( coreProductManifest, coreRootManifest, coreSDKManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/core-events/tsconfig.json b/webapp/packages/core-events/tsconfig.json index dff6765528..7e5ca46969 100644 --- a/webapp/packages/core-events/tsconfig.json +++ b/webapp/packages/core-events/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 7460c1e17c..fb8e36b3c3 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -114,6 +114,7 @@ export default [ ['ui_readonly', 'Read-only'], ['ui_test', 'Test'], ['ui_export', 'Export'], + ['ui_you', 'You'], ['root_permission_denied', "You don't have permissions"], ['root_permission_no_permission', "You don't have permission for this action"], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index 973a53da48..f98cbff0c7 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -104,6 +104,7 @@ export default [ ['ui_readonly', 'In sola lettura'], ['ui_test', 'Test'], ['ui_export', 'Export'], + ['ui_you', 'You'], ['root_permission_denied', 'Non hai i permessi'], ['app_root_session_expire_warning_title', 'La sessione sta per scadere'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index dbe87c9686..a479779496 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -110,6 +110,7 @@ export default [ ['ui_readonly', 'Доступно только для чтения'], ['ui_test', 'Проверить'], ['ui_export', 'Экспорт'], + ['ui_you', 'Вы'], ['root_permission_denied', 'Отказано в доступе'], ['root_permission_no_permission', 'У вас нет разрешения на это действие'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index ecf73a1a19..cee7130839 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -111,6 +111,7 @@ export default [ ['ui_readonly', '只读'], ['ui_test', 'Test'], ['ui_export', 'Export'], + ['ui_you', 'You'], ['root_permission_denied', '您没有权限'], ['root_permission_no_permission', '您没有权限执行此操作'], diff --git a/webapp/packages/core-navigation-tree/package.json b/webapp/packages/core-navigation-tree/package.json index 2365bad928..6ff5dbe7e6 100644 --- a/webapp/packages/core-navigation-tree/package.json +++ b/webapp/packages/core-navigation-tree/package.json @@ -38,6 +38,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts b/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts index 0992fc2e5f..fd75fb9af1 100644 --- a/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts +++ b/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts @@ -11,6 +11,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -50,6 +51,7 @@ const app = createApp( coreProjectsManifest, coreUIManifest, coreViewManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/core-navigation-tree/tsconfig.json b/webapp/packages/core-navigation-tree/tsconfig.json index 8678d53fbf..c126841b41 100644 --- a/webapp/packages/core-navigation-tree/tsconfig.json +++ b/webapp/packages/core-navigation-tree/tsconfig.json @@ -18,6 +18,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-data-context/tsconfig.json" }, diff --git a/webapp/packages/core-root/package.json b/webapp/packages/core-root/package.json index 8d9f615cec..9b8440fc73 100644 --- a/webapp/packages/core-root/package.json +++ b/webapp/packages/core-root/package.json @@ -17,6 +17,7 @@ "update-ts-references": "rimraf --glob dist && typescript-resolve-references" }, "dependencies": { + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", diff --git a/webapp/packages/core-root/src/SessionActivityService.ts b/webapp/packages/core-root/src/SessionActivityService.ts new file mode 100644 index 0000000000..cd6a5dc144 --- /dev/null +++ b/webapp/packages/core-root/src/SessionActivityService.ts @@ -0,0 +1,43 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { ClientActivityService } from '@cloudbeaver/core-client-activity'; +import { Dependency, injectable } from '@cloudbeaver/core-di'; + +import { SessionResource } from './SessionResource'; + +export const SESSION_TOUCH_TIME_PERIOD = 1000 * 60; + +@injectable() +export class SessionActivityService extends Dependency { + private touchSessionTimer: ReturnType | null = null; + + constructor(private readonly clientActivityService: ClientActivityService, private readonly sessionResource: SessionResource) { + super(); + this.notifyClientActivity = this.notifyClientActivity.bind(this); + this.clientActivityService.onActiveStateChange.addHandler(this.notifyClientActivity); + } + + private async notifyClientActivity() { + if (this.touchSessionTimer || !this.clientActivityService.isActive) { + return; + } + + try { + await this.sessionResource.updateSession(); + } catch (e) { + console.error('Session update error', e); + } + + this.touchSessionTimer = setTimeout(() => { + if (this.touchSessionTimer) { + clearTimeout(this.touchSessionTimer); + this.touchSessionTimer = null; + } + }, SESSION_TOUCH_TIME_PERIOD); + } +} diff --git a/webapp/packages/core-root/src/SessionExpireService.ts b/webapp/packages/core-root/src/SessionExpireService.ts index 35ff9cbba9..af8acf44c4 100644 --- a/webapp/packages/core-root/src/SessionExpireService.ts +++ b/webapp/packages/core-root/src/SessionExpireService.ts @@ -9,16 +9,26 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { EServerErrorCode, GQLError, GraphQLService, SessionError } from '@cloudbeaver/core-sdk'; import { errorOf } from '@cloudbeaver/core-utils'; + +export const SESSION_EXPIRE_MIN_TIME = 5 * 1000 * 60; + @injectable() export class SessionExpireService extends Bootstrap { - expired = false; + private isExpired = false; onSessionExpire: IExecutor; constructor(private readonly graphQLService: GraphQLService) { super(); + + this.sessionExpired = this.sessionExpired.bind(this); + this.onSessionExpire = new Executor(); } + get expired() { + return this.isExpired; + } + register(): void { this.graphQLService.registerInterceptor(this.sessionExpiredInterceptor.bind(this)); } @@ -32,7 +42,7 @@ export class SessionExpireService extends Bootstrap { const e = new SessionError('Session expired'); this.graphQLService.blockRequests(e); - this.expired = true; + this.isExpired = true; this.onSessionExpire.execute(); } diff --git a/webapp/packages/core-root/src/SessionResource.ts b/webapp/packages/core-root/src/SessionResource.ts index 128ab591d1..b9479333a5 100644 --- a/webapp/packages/core-root/src/SessionResource.ts +++ b/webapp/packages/core-root/src/SessionResource.ts @@ -42,6 +42,11 @@ export class SessionResource extends CachedDataResource { sessionInfoEventHandler.onEvent( ServerEventId.CbSessionState, event => { + if (this.data) { + this.data.valid = event.isValid ?? this.data.valid; + this.data.remainingTime = event.remainingTime; + // TODO: probably we want to call here this.dataUpdate + } this.onStatusUpdate.execute(event); }, undefined, @@ -68,13 +73,6 @@ export class SessionResource extends CachedDataResource { this.defaultLocale = defaultLocale; } - //! this method results in onDataUpdate handler skipping - async refreshSilent(): Promise { - const session = await this.loader(); - - this.setData(session); - } - async changeLanguage(locale: string): Promise { if (this.data?.locale === locale) { return; @@ -95,6 +93,18 @@ export class SessionResource extends CachedDataResource { return session; } + async updateSession() { + if (!this.data?.valid) { + return; + } + + const { updateSession } = await this.graphQLService.sdk.updateSession(); + + this.setData(updateSession); + + return updateSession; + } + protected setData(data: SessionState | null) { if (!this.action) { this.action = data?.actionParameters; diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 73db8658cd..8021c7a595 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -22,6 +22,7 @@ export * from './SessionDataResource'; export * from './SessionSettingsService'; export * from './SessionExpireService'; export * from './SessionExpireEventService'; +export * from './SessionActivityService'; export * from './ServerNodeService'; export * from './WindowEventsService'; export * from './manifest'; diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts index fcca411e18..5bfb8c7871 100644 --- a/webapp/packages/core-root/src/manifest.ts +++ b/webapp/packages/core-root/src/manifest.ts @@ -18,6 +18,7 @@ import { ServerNodeService } from './ServerNodeService'; import { ServerSettingsResolverService } from './ServerSettingsResolverService'; import { ServerSettingsService } from './ServerSettingsService'; import { SessionActionService } from './SessionActionService'; +import { SessionActivityService } from './SessionActivityService'; import { SessionDataResource } from './SessionDataResource'; import { SessionEventSource } from './SessionEventSource'; import { SessionExpireEventService } from './SessionExpireEventService'; @@ -53,6 +54,7 @@ export const coreRootManifest: PluginManifest = { ServerConfigEventHandler, SessionEventSource, SessionInfoEventHandler, + SessionActivityService, DataSynchronizationService, SessionPermissionEventHandler, ServerSettingsResolverService, diff --git a/webapp/packages/core-root/tsconfig.json b/webapp/packages/core-root/tsconfig.json index aa6e304abb..e13ad37489 100644 --- a/webapp/packages/core-root/tsconfig.json +++ b/webapp/packages/core-root/tsconfig.json @@ -11,6 +11,9 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/core-sdk/src/queries/session/updateSession.gql b/webapp/packages/core-sdk/src/queries/session/updateSession.gql new file mode 100644 index 0000000000..7f2e18ae49 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/session/updateSession.gql @@ -0,0 +1,5 @@ +mutation updateSession { + updateSession { + ...SessionState + } +} diff --git a/webapp/packages/core-settings-localization/package.json b/webapp/packages/core-settings-localization/package.json index 2b8bd9af21..b35223ce4c 100644 --- a/webapp/packages/core-settings-localization/package.json +++ b/webapp/packages/core-settings-localization/package.json @@ -28,6 +28,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-product": "~0.1.0", diff --git a/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts b/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts index 137f52adfb..2f21b81772 100644 --- a/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts +++ b/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; import { coreProductManifest } from '@cloudbeaver/core-product'; @@ -33,6 +34,7 @@ const app = createApp( coreRootManifest, coreSDKManifest, coreBrowserManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/core-settings-localization/tsconfig.json b/webapp/packages/core-settings-localization/tsconfig.json index 07cec65421..2cce098093 100644 --- a/webapp/packages/core-settings-localization/tsconfig.json +++ b/webapp/packages/core-settings-localization/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/core-theming/package.json b/webapp/packages/core-theming/package.json index 38b524a3ad..6050b1931b 100644 --- a/webapp/packages/core-theming/package.json +++ b/webapp/packages/core-theming/package.json @@ -31,6 +31,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/core-theming/src/ThemeSettingsService.test.ts b/webapp/packages/core-theming/src/ThemeSettingsService.test.ts index 8a44f7f6aa..83eeda9986 100644 --- a/webapp/packages/core-theming/src/ThemeSettingsService.test.ts +++ b/webapp/packages/core-theming/src/ThemeSettingsService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -35,6 +36,7 @@ const app = createApp( coreSettingsManifest, coreBrowserManifest, coreLocalizationManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/core-theming/src/styles/_form-controls.scss b/webapp/packages/core-theming/src/styles/_form-controls.scss index 6e7be79893..a029b59cf6 100644 --- a/webapp/packages/core-theming/src/styles/_form-controls.scss +++ b/webapp/packages/core-theming/src/styles/_form-controls.scss @@ -26,6 +26,23 @@ overflow: hidden; text-overflow: ellipsis; min-height: 24px; + &[type='search']::-ms-clear { + display: none; + width: 0; + height: 0; + } + &[type='search']::-ms-reveal { + display: none; + width: 0; + height: 0; + } + /* clears the ‘X’ from Chrome */ + &[type='search']::-webkit-search-decoration, + &[type='search']::-webkit-search-cancel-button, + &[type='search']::-webkit-search-results-button, + &[type='search']::-webkit-search-results-decoration { + display: none; + } } input, diff --git a/webapp/packages/core-theming/tsconfig.json b/webapp/packages/core-theming/tsconfig.json index 27d72bccb4..0f4fc71597 100644 --- a/webapp/packages/core-theming/tsconfig.json +++ b/webapp/packages/core-theming/tsconfig.json @@ -9,6 +9,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx index 0e85707f45..df5410df09 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { Group, GroupTitle, InputField, useResource, useTranslate } from '@cloudbeaver/core-blocks'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { ServerConfigResource, SESSION_EXPIRE_MIN_TIME, SESSION_TOUCH_TIME_PERIOD } from '@cloudbeaver/core-root'; import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; @@ -16,6 +16,8 @@ interface Props { state: IServerConfigurationPageState; } +const INPUT_MIN_SESSION_EXPIRE_TIME = Math.ceil((SESSION_EXPIRE_MIN_TIME + SESSION_TOUCH_TIME_PERIOD) / 60000) + 1; + export const ServerConfigurationInfoForm = observer(function ServerConfigurationInfoForm({ state }) { const serverConfigLoader = useResource(ServerConfigurationInfoForm, ServerConfigResource, undefined); const translate = useTranslate(); @@ -41,7 +43,7 @@ export const ServerConfigurationInfoForm = observer(function ServerConfig type="number" name="sessionExpireTime" state={state.serverConfig} - min={1} + min={INPUT_MIN_SESSION_EXPIRE_TIME} mapState={(v: number | undefined) => String((v === 0 ? 60000 : v ?? 1800000) / 1000 / 60)} mapValue={(v?: string) => (v === undefined ? 30 : Number(v) || 1) * 1000 * 60} required diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx index 6f36938940..af186ae5e1 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUserList.tsx @@ -26,6 +26,7 @@ import { } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { TLocalizationToken } from '@cloudbeaver/core-localization'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { getFilteredUsers } from './getFilteredUsers'; @@ -47,6 +48,7 @@ export const GrantedUserList = observer(function GrantedUserList({ grante const translate = useTranslate(); const usersResource = useService(UsersResource); + const serverConfigResource = useService(ServerConfigResource); const [selectedSubjects] = useState>(() => observable(new Map())); const [filterState] = useState(() => observable({ filterValue: '' })); @@ -70,6 +72,14 @@ export const GrantedUserList = observer(function GrantedUserList({ grante } } + function isEditable(userId: string) { + if (serverConfigResource.distributed) { + return true; + } + + return !usersResource.isActiveUser(userId); + } + return (
@@ -82,12 +92,7 @@ export const GrantedUserList = observer(function GrantedUserList({ grante
- !usersResource.isActiveUser(item)} - > +
isEditable(item)}> {tableInfoText && ( @@ -95,20 +100,17 @@ export const GrantedUserList = observer(function GrantedUserList({ grante {translate(tableInfoText)} )} - {users.map(user => { - const activeUser = usersResource.isActiveUser(user.userId); - return ( - - ); - })} + {users.map(user => ( + + ))}
diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx index fdf08fc0da..21cb1e9bbf 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/UserList.tsx @@ -25,6 +25,7 @@ import { useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { getFilteredUsers } from './getFilteredUsers'; @@ -46,6 +47,7 @@ export const UserList = observer(function UserList({ userList, grantedUse const translate = useTranslate(); const usersResource = useService(UsersResource); + const serverConfigResource = useService(ServerConfigResource); const [selectedSubjects] = useState>(() => observable(new Map())); const [filterState] = useState(() => observable({ filterValue: '' })); @@ -60,6 +62,14 @@ export const UserList = observer(function UserList({ userList, grantedUse selectedSubjects.clear(); }, []); + function isEditable(userId: string) { + if (serverConfigResource.distributed) { + return true; + } + + return !usersResource.isActiveUser(userId); + } + return (
@@ -73,7 +83,7 @@ export const UserList = observer(function UserList({ userList, grantedUse className={s(styles, { table: true })} keys={keys} selectedItems={selectedSubjects} - isItemSelectable={item => !(usersResource.isActiveUser(item) || grantedUsers.includes(item))} + isItemSelectable={item => isEditable(item) && !grantedUsers.includes(item)} > @@ -82,20 +92,17 @@ export const UserList = observer(function UserList({ userList, grantedUse {translate('ui_search_no_result_placeholder')} )} - {users.map(user => { - const activeUser = usersResource.isActiveUser(user.userId); - return ( - - ); - })} + {users.map(user => ( + + ))}
diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx index ec997bd6e7..e813c5b928 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx @@ -36,7 +36,6 @@ interface Props { export const User = observer(function User({ user, displayAuthRole, selectable }) { const usersAdministrationService = useService(UsersAdministrationService); - const teams = user.grantedTeams.join(', '); const usersService = useService(UsersResource); const notificationService = useService(NotificationService); const administrationUsersManagementService = useService(AdministrationUsersManagementService); @@ -57,6 +56,7 @@ export const User = observer(function User({ user, displayAuthRole, selec : undefined; const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; + const teams = user.grantedTeams.join(', '); return ( diff --git a/webapp/packages/plugin-data-export/package.json b/webapp/packages/plugin-data-export/package.json index 14308b2ca4..cca56232f5 100644 --- a/webapp/packages/plugin-data-export/package.json +++ b/webapp/packages/plugin-data-export/package.json @@ -45,6 +45,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts index 857d4e7ef9..a312548387 100644 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts +++ b/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts @@ -12,6 +12,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; @@ -68,6 +69,7 @@ const app = createApp( navigationTabsPlugin, objectViewerManifest, dataViewerManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/plugin-data-export/tsconfig.json b/webapp/packages/plugin-data-export/tsconfig.json index bd41a73288..9c21a8add0 100644 --- a/webapp/packages/plugin-data-export/tsconfig.json +++ b/webapp/packages/plugin-data-export/tsconfig.json @@ -21,6 +21,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-connections/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index 8d81074eb7..35d922a02e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -48,6 +48,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts index 6b0b4c495f..99ffe65f11 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts @@ -12,6 +12,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; @@ -68,6 +69,7 @@ const app = createApp( navigationTabsPlugin, objectViewerManifest, dataViewerManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json index cdce42e1b9..5a51933c77 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json +++ b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-connections/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-viewer/package.json b/webapp/packages/plugin-data-viewer/package.json index a2ceda3a6c..ee5db29317 100644 --- a/webapp/packages/plugin-data-viewer/package.json +++ b/webapp/packages/plugin-data-viewer/package.json @@ -50,6 +50,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts index b496ab7f2d..522eac178f 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts @@ -12,6 +12,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; @@ -66,6 +67,7 @@ const app = createApp( navigationTreePlugin, navigationTabsPlugin, objectViewerManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/plugin-data-viewer/tsconfig.json b/webapp/packages/plugin-data-viewer/tsconfig.json index 83e81ca240..af109b34e3 100644 --- a/webapp/packages/plugin-data-viewer/tsconfig.json +++ b/webapp/packages/plugin-data-viewer/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-connections/tsconfig.json" }, diff --git a/webapp/packages/plugin-devtools/package.json b/webapp/packages/plugin-devtools/package.json index 6473e7b519..024ae52605 100644 --- a/webapp/packages/plugin-devtools/package.json +++ b/webapp/packages/plugin-devtools/package.json @@ -29,12 +29,12 @@ "@cloudbeaver/plugin-user-profile": "~0.1.0", "mobx": "^6.12.0", "mobx-react-lite": "^4.0.5", - "react": "^18.2.0", - "reshadow": "^0.0.1" + "react": "^18.2.0" }, "peerDependencies": {}, "devDependencies": { "@types/react": "^18.2.42", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "typescript-plugin-css-modules": "^5.0.2" } } diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.m.css b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.m.css new file mode 100644 index 0000000000..1fad1e69ee --- /dev/null +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.m.css @@ -0,0 +1,3 @@ +.searchBox { + padding: 8px 12px; +} diff --git a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx index 93947cf69e..6c76a8351a 100644 --- a/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx +++ b/webapp/packages/plugin-devtools/src/ContextMenu/SearchResourceMenuItemComponent.tsx @@ -6,41 +6,36 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import styled, { css } from 'reshadow'; -import { useStyles } from '@cloudbeaver/core-blocks'; +import { s, useS } from '@cloudbeaver/core-blocks'; import type { IContextMenuItemProps } from '@cloudbeaver/core-ui'; import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_MENU_SEARCH } from './DATA_CONTEXT_MENU_SEARCH'; - -const styles = css` - search-box { - padding: 8px 12px; - } -`; +import styles from './SearchResourceMenuItemComponent.m.css'; export const SearchResourceMenuItemComponent: ICustomMenuItemComponent = observer(function SearchResourceMenuItemComponent({ item, onClick, menuData, className, - style, }) { + const style = useS(styles); const value = menuData.context.tryGet(DATA_CONTEXT_MENU_SEARCH) ?? ''; function handleChange(value: string) { menuData.context.set(DATA_CONTEXT_MENU_SEARCH, value); } - return styled(useStyles(style, styles))( - + return ( +
handleChange(event.target.value)} /> - , +
); }); diff --git a/webapp/packages/plugin-log-viewer/package.json b/webapp/packages/plugin-log-viewer/package.json index 56ad8ff9e9..5ac07403b3 100644 --- a/webapp/packages/plugin-log-viewer/package.json +++ b/webapp/packages/plugin-log-viewer/package.json @@ -39,6 +39,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts index 1c70c2b82f..3ae780ca4e 100644 --- a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts +++ b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts @@ -11,6 +11,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -48,6 +49,7 @@ const app = createApp( coreAppManifest, coreRoutingManifest, coreThemingManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/plugin-log-viewer/tsconfig.json b/webapp/packages/plugin-log-viewer/tsconfig.json index ba06aa67a9..8849b8f400 100644 --- a/webapp/packages/plugin-log-viewer/tsconfig.json +++ b/webapp/packages/plugin-log-viewer/tsconfig.json @@ -21,6 +21,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/plugin-navigation-tree/package.json b/webapp/packages/plugin-navigation-tree/package.json index 9a43574cb0..249e835536 100644 --- a/webapp/packages/plugin-navigation-tree/package.json +++ b/webapp/packages/plugin-navigation-tree/package.json @@ -47,6 +47,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts index 4fdf772116..23980e341c 100644 --- a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts +++ b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts @@ -12,6 +12,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; @@ -61,6 +62,7 @@ const app = createApp( coreNavigationTree, coreAppManifest, coreThemingManifest, + coreClientActivityManifest, ); mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/plugin-navigation-tree/tsconfig.json b/webapp/packages/plugin-navigation-tree/tsconfig.json index 44067ff3ca..6edb4662a5 100644 --- a/webapp/packages/plugin-navigation-tree/tsconfig.json +++ b/webapp/packages/plugin-navigation-tree/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-connections/tsconfig.json" }, diff --git a/webapp/packages/plugin-resource-manager/package.json b/webapp/packages/plugin-resource-manager/package.json index 6454b5bd6f..39633b3548 100644 --- a/webapp/packages/plugin-resource-manager/package.json +++ b/webapp/packages/plugin-resource-manager/package.json @@ -33,6 +33,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-plugin": "~0.1.0", diff --git a/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts b/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts index f72f4ffca2..c63be311bb 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts @@ -8,6 +8,7 @@ import '@testing-library/jest-dom'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; @@ -35,6 +36,7 @@ const app = createApp( coreSettingsManifest, coreBrowserManifest, coreLocalizationManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint)); diff --git a/webapp/packages/plugin-resource-manager/tsconfig.json b/webapp/packages/plugin-resource-manager/tsconfig.json index bd08ea8c68..98ccf5e9f7 100644 --- a/webapp/packages/plugin-resource-manager/tsconfig.json +++ b/webapp/packages/plugin-resource-manager/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, diff --git a/webapp/packages/plugin-session-expiration/package.json b/webapp/packages/plugin-session-expiration/package.json index 11c81689ad..ad3de79c63 100644 --- a/webapp/packages/plugin-session-expiration/package.json +++ b/webapp/packages/plugin-session-expiration/package.json @@ -22,6 +22,7 @@ "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", diff --git a/webapp/packages/plugin-session-expiration/src/LocaleService.ts b/webapp/packages/plugin-session-expiration/src/LocaleService.ts new file mode 100644 index 0000000000..edeb82d255 --- /dev/null +++ b/webapp/packages/plugin-session-expiration/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise {} + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + case 'it': + return (await import('./locales/it')).default; + case 'zh': + return (await import('./locales/zh')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/plugin-session-expiration/src/SessionExpireWarningDialog/SessionExpireWarningDialogBootstrap.ts b/webapp/packages/plugin-session-expiration/src/SessionExpireWarningDialog/SessionExpireWarningDialogBootstrap.ts index 7db83e316b..7567820fb4 100644 --- a/webapp/packages/plugin-session-expiration/src/SessionExpireWarningDialog/SessionExpireWarningDialogBootstrap.ts +++ b/webapp/packages/plugin-session-expiration/src/SessionExpireWarningDialog/SessionExpireWarningDialogBootstrap.ts @@ -6,15 +6,14 @@ * you may not use this file except in compliance with the License. */ import { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ServerConfigResource, SessionExpireService, SessionResource } from '@cloudbeaver/core-root'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ServerConfigResource, SESSION_EXPIRE_MIN_TIME, SessionExpireService, SessionResource } from '@cloudbeaver/core-root'; import { GraphQLService } from '@cloudbeaver/core-sdk'; -import { SessionExpireWarningDialog } from './SessionExpireWarningDialog'; - -const WARN_IN = 5 * 1000 * 60; - +const SessionExpireWarningDialog = importLazyComponent(() => import('./SessionExpireWarningDialog').then(m => m.SessionExpireWarningDialog)); @injectable() export class SessionExpireWarningDialogBootstrap extends Bootstrap { private dialogInternalPromise: Promise | null; @@ -25,6 +24,7 @@ export class SessionExpireWarningDialogBootstrap extends Bootstrap { private readonly sessionResource: SessionResource, private readonly userInfoResource: UserInfoResource, private readonly graphQLService: GraphQLService, + private readonly notificationService: NotificationService, ) { super(); this.dialogInternalPromise = null; @@ -52,12 +52,12 @@ export class SessionExpireWarningDialogBootstrap extends Bootstrap { const sessionDuration = this.serverConfigResource.data?.sessionExpireTime; - if (this.sessionExpireService.expired || !sessionDuration || sessionDuration < WARN_IN) { + if (this.sessionExpireService.expired || !sessionDuration || sessionDuration < SESSION_EXPIRE_MIN_TIME) { this.close(); return; } - if (remainingTime !== undefined && remainingTime < WARN_IN) { + if (remainingTime !== undefined && remainingTime <= SESSION_EXPIRE_MIN_TIME) { this.open(); } else { this.close(); @@ -74,7 +74,11 @@ export class SessionExpireWarningDialogBootstrap extends Bootstrap { const { sessionState } = await this.graphQLService.sdk.sessionState(); if (sessionState.valid) { - await this.sessionResource.refreshSilent(); + try { + await this.sessionResource.updateSession(); + } catch (e: any) { + this.notificationService.logException(e, 'plugin_session_expiration_session_update_error'); + } } else { this.sessionExpireService.sessionExpired(); } diff --git a/webapp/packages/plugin-session-expiration/src/locales/en.ts b/webapp/packages/plugin-session-expiration/src/locales/en.ts new file mode 100644 index 0000000000..8c94cf3aff --- /dev/null +++ b/webapp/packages/plugin-session-expiration/src/locales/en.ts @@ -0,0 +1 @@ +export default [['plugin_session_expiration_session_update_error', 'Session update failed']]; diff --git a/webapp/packages/plugin-session-expiration/src/locales/it.ts b/webapp/packages/plugin-session-expiration/src/locales/it.ts new file mode 100644 index 0000000000..8c94cf3aff --- /dev/null +++ b/webapp/packages/plugin-session-expiration/src/locales/it.ts @@ -0,0 +1 @@ +export default [['plugin_session_expiration_session_update_error', 'Session update failed']]; diff --git a/webapp/packages/plugin-session-expiration/src/locales/ru.ts b/webapp/packages/plugin-session-expiration/src/locales/ru.ts new file mode 100644 index 0000000000..4404022cc8 --- /dev/null +++ b/webapp/packages/plugin-session-expiration/src/locales/ru.ts @@ -0,0 +1 @@ +export default [['plugin_session_expiration_session_update_error', 'Не удалось обновить сессию']]; diff --git a/webapp/packages/plugin-session-expiration/src/locales/zh.ts b/webapp/packages/plugin-session-expiration/src/locales/zh.ts new file mode 100644 index 0000000000..8c94cf3aff --- /dev/null +++ b/webapp/packages/plugin-session-expiration/src/locales/zh.ts @@ -0,0 +1 @@ +export default [['plugin_session_expiration_session_update_error', 'Session update failed']]; diff --git a/webapp/packages/plugin-session-expiration/src/manifest.ts b/webapp/packages/plugin-session-expiration/src/manifest.ts index ec5034d135..e625ebfafd 100644 --- a/webapp/packages/plugin-session-expiration/src/manifest.ts +++ b/webapp/packages/plugin-session-expiration/src/manifest.ts @@ -7,11 +7,12 @@ */ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService'; import { PluginBootstrap } from './PluginBootstrap'; import { SessionExpiredDialogBootstrap } from './SessionExpireDialog/SessionExpiredDialogBootstrap'; import { SessionExpireWarningDialogBootstrap } from './SessionExpireWarningDialog/SessionExpireWarningDialogBootstrap'; export const sessionExpirationPlugin: PluginManifest = { info: { name: 'Session Expiration plugin' }, - providers: [PluginBootstrap, SessionExpiredDialogBootstrap, SessionExpireWarningDialogBootstrap], + providers: [PluginBootstrap, SessionExpiredDialogBootstrap, SessionExpireWarningDialogBootstrap, LocaleService], }; diff --git a/webapp/packages/plugin-session-expiration/tsconfig.json b/webapp/packages/plugin-session-expiration/tsconfig.json index 9c615c7d0b..471e24ac2c 100644 --- a/webapp/packages/plugin-session-expiration/tsconfig.json +++ b/webapp/packages/plugin-session-expiration/tsconfig.json @@ -21,6 +21,9 @@ { "path": "../core-events/tsconfig.json" }, + { + "path": "../core-localization/tsconfig.json" + }, { "path": "../core-root/tsconfig.json" }, diff --git a/webapp/packages/plugin-sql-editor/package.json b/webapp/packages/plugin-sql-editor/package.json index b04c08836c..5adaad5da1 100644 --- a/webapp/packages/plugin-sql-editor/package.json +++ b/webapp/packages/plugin-sql-editor/package.json @@ -51,6 +51,7 @@ "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", + "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-connections": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts index e6e69b43f9..8730ae712e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts @@ -12,6 +12,7 @@ import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; +import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreConnectionsManifest } from '@cloudbeaver/core-connections'; import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; @@ -68,6 +69,7 @@ const app = createApp( navigationTabsPlugin, objectViewerManifest, dataViewerManifest, + coreClientActivityManifest, ); const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); diff --git a/webapp/packages/plugin-sql-editor/tsconfig.json b/webapp/packages/plugin-sql-editor/tsconfig.json index a2fa50c048..9b032c32e6 100644 --- a/webapp/packages/plugin-sql-editor/tsconfig.json +++ b/webapp/packages/plugin-sql-editor/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../core-browser/tsconfig.json" }, + { + "path": "../core-client-activity/tsconfig.json" + }, { "path": "../core-connections/tsconfig.json" },