CB-4436 update session only in touchSession (#2377)

* CB-4436 update session only in touchSession

* CB-4346 adds session touch for user actions

* CB-4346 adds core-activity package

* CB-4346 fix: mobx version core-activity package

* CB-4346 ClientActivityService cleanup

* CB-4346 ClientActivityService cleanup [2]

* CB-4346 fix: client activity function contexts bind

* CB-4346 useClientActivity cleanup

* CB-4346 removed unneeded deps

* CB-4346 session warning dialog props renaming

* CB-4364 adds client activity service setsActivity method

* CB-4346 activity timers refactor

* CB-4346 adds min time for session expire setting

* CB-4346 adds update client activity each 300ms

* CB-4346 touchSession logic moved to SessionExpiredService

* СB-4346 session expire service uses session resource to touchSession

* CB-4346 code cleanup

* CB-4346 removed unused activity package from plugin session expiration

* CB-4346 SessionExpireService - removed cycle dependency

* CB-4346 code cleanup

* CB-4346 update session state if it is invalid

* CB-4346 removed arrow function from useClientActivity

* Revert "CB-4346 SessionExpireService - removed cycle dependency"

This reverts commit adba2d5eed.

* CB-4346 touchSession logic moved from SessionExpiredService to SessionResource because of cycle dependency

* CB-4346 chore: core-activity -> core-client-activity renaming

* CB-4346 chore: vars renaming

* CB-4346 refactor: removed force from sessionResource.touchSession()

* CB-4346 adds SessionTouchService

* CB-4346 chore: SESSION_EXPIRE_MIN_TIME const new name

* CB-4346 sessionTouchService license

* CB-4346 uses SessionTouchService as Dependency

* CB-4346 pr fixes

* CB-4346 adds core-localization to plugin-session-expiration

* CB-4346 adds core coreClientActivityManifest to unit tests

* CB-4346 updates ts references

* CB-4436 new update session gql api

* CB-4346 adds updateSession API call

* CB-4346 code cleanup sessionResource

* CB-4346 do not send state event for not authorized session

* CB-4346 chore: update session info with event

* CB-4346 adds update activity on touch (tablets, phones support)

* CB-4346 add debug log

* CB-4346 force update session on create

---------

Co-authored-by: s.teleshev <s.teleshev@mu.se>
Co-authored-by: Aleksei Potsetsuev <wrouds@gmail.com>
Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Alexander Skoblikov
2024-02-23 17:02:04 +00:00
committed by GitHub
co-authored by s.teleshev Aleksei Potsetsuev Evgenia Bezborodova
parent 38987293a3
commit c0c4e1870a
82 changed files with 489 additions and 44 deletions
@@ -521,6 +521,7 @@ public class WebSession extends BaseWebSession
HttpServletRequest request,
HttpServletResponse response
) throws DBWebException {
log.debug("Update session lifetime " + getSessionId() + " for user " + getUserId());
touchSession();
HttpSession httpSession = request.getSession();
this.lastRemoteAddr = request.getRemoteAddr();
@@ -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
@@ -90,9 +90,14 @@ public interface DBWServiceCore extends DBWService {
@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;
@@ -99,6 +99,8 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
.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")))
@@ -29,6 +29,7 @@ import io.cloudbeaver.server.CBApplication;
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 class WebServiceCore implements DBWServiceCore {
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);
@@ -93,12 +93,11 @@ public class WebSessionManager {
}
@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 class WebSessionManager {
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 class WebSessionManager {
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 class WebSessionManager {
*/
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);
}
});
}
}
@@ -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",
@@ -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));
@@ -9,6 +9,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-data-context/tsconfig.json"
},
+1
View File
@@ -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",
+2
View File
@@ -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 (
<DNDProvider>
@@ -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();
};
}, []);
}
+3
View File
@@ -9,6 +9,9 @@
{
"path": "../core-blocks/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.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",
@@ -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));
@@ -14,6 +14,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-data-context/tsconfig.json"
},
+1
View File
@@ -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",
@@ -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));
@@ -12,6 +12,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.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",
@@ -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,
];
@@ -27,6 +27,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-connections/tsconfig.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",
@@ -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));
@@ -14,6 +14,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.json"
},
@@ -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*
@@ -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"
}
}
@@ -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<typeof setTimeout> | null;
public isActive: boolean;
public onActiveStateChange: IExecutor<boolean>;
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);
}
}
@@ -0,0 +1,2 @@
export * from './ClientActivityService';
export * from './manifest';
@@ -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],
};
@@ -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/**/*"
]
}
+1
View File
@@ -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",
@@ -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));
@@ -9,6 +9,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.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",
@@ -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));
@@ -18,6 +18,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-data-context/tsconfig.json"
},
+1
View File
@@ -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",
@@ -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<typeof setTimeout> | 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);
}
}
@@ -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();
}
@@ -42,6 +42,11 @@ export class SessionResource extends CachedDataResource<SessionState | null> {
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<SessionState | null> {
this.defaultLocale = defaultLocale;
}
//! this method results in onDataUpdate handler skipping
async refreshSilent(): Promise<void> {
const session = await this.loader();
this.setData(session);
}
async changeLanguage(locale: string): Promise<void> {
if (this.data?.locale === locale) {
return;
@@ -95,6 +93,18 @@ export class SessionResource extends CachedDataResource<SessionState | null> {
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;
+1
View File
@@ -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';
@@ -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,
+3
View File
@@ -11,6 +11,9 @@
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
},
"references": [
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.json"
},
@@ -0,0 +1,5 @@
mutation updateSession {
updateSession {
...SessionState
}
}
@@ -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",
@@ -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));
@@ -9,6 +9,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.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",
@@ -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));
@@ -9,6 +9,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.json"
},
@@ -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<Props>(function ServerConfigurationInfoForm({ state }) {
const serverConfigLoader = useResource(ServerConfigurationInfoForm, ServerConfigResource, undefined);
const translate = useTranslate();
@@ -41,7 +43,7 @@ export const ServerConfigurationInfoForm = observer<Props>(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
@@ -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",
@@ -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));
@@ -21,6 +21,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-connections/tsconfig.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",
@@ -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));
@@ -24,6 +24,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-connections/tsconfig.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",
@@ -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));
@@ -24,6 +24,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-connections/tsconfig.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",
@@ -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));
@@ -21,6 +21,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.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",
@@ -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));
@@ -24,6 +24,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-connections/tsconfig.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",
@@ -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));
@@ -12,6 +12,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.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",
@@ -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<void> {
this.localizationService.addProvider(this.provider.bind(this));
}
load(): void | Promise<void> {}
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;
}
}
}
@@ -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<DialogueStateResult | null> | 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();
}
@@ -0,0 +1 @@
export default [['plugin_session_expiration_session_update_error', 'Session update failed']];
@@ -0,0 +1 @@
export default [['plugin_session_expiration_session_update_error', 'Session update failed']];
@@ -0,0 +1 @@
export default [['plugin_session_expiration_session_update_error', 'Не удалось обновить сессию']];
@@ -0,0 +1 @@
export default [['plugin_session_expiration_session_update_error', 'Session update failed']];
@@ -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],
};
@@ -21,6 +21,9 @@
{
"path": "../core-events/tsconfig.json"
},
{
"path": "../core-localization/tsconfig.json"
},
{
"path": "../core-root/tsconfig.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",
@@ -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));
@@ -24,6 +24,9 @@
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-connections/tsconfig.json"
},