diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 810c387d47..87f9b85409 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -258,4 +258,12 @@ public WSEventController getEventController() { public boolean isEnvironmentVariablesAccessible() { return false; } + + protected void closeResource(String name, Runnable closeFunction) { + try { + closeFunction.run(); + } catch (Exception e) { + log.error("Failed close " + name, e); + } + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java index 1c8fa96c2c..82cbfeb05a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebServerConfiguration.java @@ -16,6 +16,11 @@ */ package io.cloudbeaver.model.app; +import io.cloudbeaver.server.WebServerPreferenceStore; +import org.jkiss.code.NotNull; + +import java.util.Map; + /** * Web server configuration. * Contains only server configuration properties. @@ -27,4 +32,12 @@ default String getRootURI() { return ""; } + /** + * @return the setting values that will be used in {@link WebServerPreferenceStore} + */ + @NotNull + default Map getProductSettings() { + return Map.of(); + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java similarity index 93% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java index a40bf64584..681262bf36 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPreferenceStore.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/server/WebServerPreferenceStore.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.server; +import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.impl.preferences.AbstractPreferenceStore; import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; @@ -23,16 +24,12 @@ import java.io.IOException; import java.util.Map; -public class CBPreferenceStore extends AbstractPreferenceStore { - @NotNull - private final CBPlatform cbPlatform; +public class WebServerPreferenceStore extends AbstractPreferenceStore { private final DBPPreferenceStore parentStore; - public CBPreferenceStore( - @NotNull CBPlatform cbPlatform, + public WebServerPreferenceStore( @NotNull DBPPreferenceStore parentStore ) { - this.cbPlatform = cbPlatform; this.parentStore = parentStore; } @@ -188,7 +185,7 @@ public void save() throws IOException { } private Map productConf() { - var app = cbPlatform.getApplication(); - return app.getProductConfiguration(); + var app = WebAppUtils.getWebApplication(); + return app.getServerConfiguration().getProductSettings(); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java index 1c4e0fadd5..0cc1833109 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBPlatform.java @@ -59,7 +59,7 @@ public class CBPlatform extends BaseGQLPlatform { @Nullable private static GQLApplicationAdapter application = null; - private CBPreferenceStore preferenceStore; + private WebServerPreferenceStore preferenceStore; protected final List applicableDrivers = new ArrayList<>(); public static CBPlatform getInstance() { @@ -77,7 +77,7 @@ public static void setApplication(@NotNull GQLApplicationAdapter application) { protected synchronized void initialize() { long startTime = System.currentTimeMillis(); log.info("Initialize web platform...: "); - this.preferenceStore = new CBPreferenceStore(this, WebPlatformActivator.getInstance().getPreferences()); + this.preferenceStore = new WebServerPreferenceStore(WebPlatformActivator.getInstance().getPreferences()); super.initialize(); refreshApplicableDrivers(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java index f6c68805f8..bcf97f7aaf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/actions/AbstractActionServletHandler.java @@ -18,6 +18,7 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWServletHandler; +import io.cloudbeaver.utils.WebAppUtils; import jakarta.servlet.Servlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -43,7 +44,7 @@ protected void createActionFromParams(WebSession session, HttpServletRequest req action.saveInSession(session); // Redirect to home - response.sendRedirect("/"); + response.sendRedirect(WebAppUtils.getWebApplication().getServerConfiguration().getRootURI()); } protected abstract String getActionConsole(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 4c7cfde0cc..e3c94cce88 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -24,7 +24,6 @@ import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.server.servlets.CBStatusServlet; -import io.cloudbeaver.server.servlets.ProxyResourceHandler; import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; import io.cloudbeaver.service.DBWServiceBindingServlet; import io.cloudbeaver.service.DBWServiceBindingWebSocket; @@ -101,11 +100,12 @@ public void runServer() { String rootURI = serverConfiguration.getRootURI(); servletContextHandler.setContextPath(rootURI); - ServletHolder staticServletHolder = new ServletHolder("static", new CBStaticServlet()); + ServletHolder staticServletHolder = new ServletHolder( + "static", new CBStaticServlet(Path.of(serverConfiguration.getContentRoot())) + ); staticServletHolder.setInitParameter("dirAllowed", "false"); staticServletHolder.setInitParameter("cacheControl", "public, max-age=" + CBStaticServlet.STATIC_CACHE_SECONDS); servletContextHandler.addServlet(staticServletHolder, "/"); - servletContextHandler.insertHandler(new ProxyResourceHandler(Path.of(serverConfiguration.getContentRoot()))); if (Files.isSymbolicLink(contentRootPath)) { servletContextHandler.addAliasCheck(new CBSymLinkContentAllowedAliasChecker(contentRootPath)); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java deleted file mode 100644 index 15d4de80d8..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/PeriodicSystemJob.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.jobs; - -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.app.DBPPlatform; -import org.jkiss.dbeaver.model.runtime.AbstractJob; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -public abstract class PeriodicSystemJob extends AbstractJob { - - @NotNull - protected final DBPPlatform platform; - private final long periodMs; - - public PeriodicSystemJob(@NotNull String name, @NotNull DBPPlatform platform, long periodMs) { - super(name); - this.platform = platform; - this.periodMs = periodMs; - - setUser(false); - setSystem(true); - } - - @Override - protected IStatus run(@NotNull DBRProgressMonitor monitor) { - if (platform.isShuttingDown()) { - return Status.OK_STATUS; - } - - doJob(monitor); - - // If the platform is still running after the job is completed, reschedule the job - if (!platform.isShuttingDown()) { - scheduleMonitor(); - } - - return Status.OK_STATUS; - } - - protected abstract void doJob(@NotNull DBRProgressMonitor monitor); - - public void scheduleMonitor() { - schedule(periodMs); - } -} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java index 354934ebc4..01aaffb15a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -21,8 +21,9 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; -public class SessionStateJob extends PeriodicSystemJob { +public class SessionStateJob extends PeriodicJob { private static final Log log = Log.getLog(SessionStateJob.class); private static final int PERIOD_MS = 30_000; // once per 30 seconds private final WebSessionManager sessionManager; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java index c17dc270f4..e73dacdf62 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/WebSessionMonitorJob.java @@ -21,11 +21,12 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPPlatform; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.PeriodicJob; /** * WebSessionMonitorJob */ -public class WebSessionMonitorJob extends PeriodicSystemJob { +public class WebSessionMonitorJob extends PeriodicJob { private static final Log log = Log.getLog(WebSessionMonitorJob.class); private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds private final WebSessionManager sessionManager; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java index e558dd4c75..232d57f3f9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/CBStaticServlet.java @@ -35,14 +35,23 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.http.HttpHeader; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthProvider; import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; @WebServlet(urlPatterns = "/") @@ -54,6 +63,13 @@ public class CBStaticServlet extends DefaultServlet { private static final Log log = Log.getLog(CBStaticServlet.class); + @NotNull + private final Path contentRoot; + + public CBStaticServlet(@NotNull Path contentRoot) { + this.contentRoot = contentRoot; + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { for (WebServletHandlerDescriptor handler : WebHandlerRegistry.getInstance().getServletHandlers()) { @@ -83,7 +99,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } catch (DBWebException e) { log.error("Error reading websession", e); } - super.doGet(request, response); + patchStaticContentIfNeeded(request, response); } private void performAutoLoginIfNeeded(HttpServletRequest request, WebSession webSession) { @@ -177,4 +193,40 @@ private boolean processSessionStart(HttpServletRequest request, HttpServletRespo return false; } + private void patchStaticContentIfNeeded(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String pathInContext = request.getServletPath(); + + if ("/".equals(pathInContext)) { + pathInContext = "index.html"; + } + + if (pathInContext == null || !pathInContext.endsWith("index.html") + && !pathInContext.endsWith("sso.html") + && !pathInContext.endsWith("ssoError.html") + ) { + super.doGet(request, response); + return; + } + + if (pathInContext.startsWith("/")) { + pathInContext = pathInContext.substring(1); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + var filePath = contentRoot.resolve(pathInContext); + try (InputStream fis = Files.newInputStream(filePath)) { + IOUtils.copyStream(fis, baos); + } + String indexContents = baos.toString(StandardCharsets.UTF_8); + CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); + indexContents = indexContents + .replace("{ROOT_URI}", serverConfig.getRootURI()) + .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + // Disable cache for index.html + response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); + response.setHeader(HttpHeader.EXPIRES.toString(), "0"); + response.getOutputStream().write(ByteBuffer.wrap(indexBytes)); + } + } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java deleted file mode 100644 index c9c20d874b..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/servlets/ProxyResourceHandler.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.cloudbeaver.server.servlets; - -import io.cloudbeaver.model.config.CBServerConfig; -import io.cloudbeaver.server.CBApplication; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; -import org.jkiss.code.NotNull; -import org.jkiss.utils.IOUtils; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ProxyResourceHandler extends Handler.Wrapper { - @NotNull - private final Path contentRoot; - - public ProxyResourceHandler(@NotNull Path contentRoot) { - this.contentRoot = contentRoot; - } - - public boolean handle(Request request, Response response, Callback callback) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - String pathInContext = Request.getPathInContext(request); - - if ("/".equals(pathInContext)) { - pathInContext = "index.html"; - } - - if (pathInContext == null || !pathInContext.endsWith("index.html") - && !pathInContext.endsWith("sso.html") - && !pathInContext.endsWith("ssoError.html") - ) { - return super.handle(request, response, callback); - } - - if (pathInContext.startsWith("/")) { - pathInContext = pathInContext.substring(1); - } - var filePath = contentRoot.resolve(pathInContext); - try (InputStream fis = Files.newInputStream(filePath)) { - IOUtils.copyStream(fis, baos); - } - String indexContents = baos.toString(StandardCharsets.UTF_8); - CBServerConfig serverConfig = CBApplication.getInstance().getServerConfiguration(); - indexContents = indexContents - .replace("{ROOT_URI}", serverConfig.getRootURI()) - .replace("{STATIC_CONTENT}", serverConfig.getStaticContent()); - byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); - - // Disable cache for index.html - response.getHeaders().put(HttpHeader.CACHE_CONTROL.toString(), "no-cache, no-store, must-revalidate"); - response.getHeaders().put(HttpHeader.EXPIRES.toString(), "0"); - - response.write(true, ByteBuffer.wrap(indexBytes), callback); - return true; - } -} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java index 9f31bf115f..42d2024e83 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/local/LocalServletHandler.java @@ -40,7 +40,7 @@ public class LocalServletHandler extends AbstractActionServletHandler { @Override public boolean handleRequest(Servlet servlet, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { - if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getPathInfo()))) { + if (URI_PREFIX.equals(WebAppUtils.removeSideSlashes(request.getServletPath()))) { try { WebSession webSession = CBPlatform.getInstance().getSessionManager().getWebSession(request, response, true); createActionFromParams(webSession, request, response); diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 806dbd4e6b..f8ac85aa1c 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -2082,7 +2082,7 @@ public SMAuthInfo finishAuthentication(@NotNull String authId) throws DBExceptio return finishAuthentication(authInfo, false, authInfo.isForceSessionsLogout()); } - private SMAuthInfo finishAuthentication( + protected SMAuthInfo finishAuthentication( @NotNull SMAuthInfo authInfo, boolean isSyncAuth, boolean forceSessionsLogout @@ -3134,7 +3134,7 @@ private void deleteAuthSubject(Connection dbCon, String subjectId) throws SQLExc } } - private WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { + protected WebAuthProviderDescriptor getAuthProvider(String authProviderId) throws DBCException { WebAuthProviderDescriptor authProvider = WebAuthProviderRegistry.getInstance().getAuthProvider(authProviderId); if (authProvider == null) { throw new DBCException("Auth provider not found: " + authProviderId); diff --git a/webapp/packages/core-app/src/AppScreen/RightArea.tsx b/webapp/packages/core-app/src/AppScreen/RightArea.tsx index 540a728ea2..34097d383f 100644 --- a/webapp/packages/core-app/src/AppScreen/RightArea.tsx +++ b/webapp/packages/core-app/src/AppScreen/RightArea.tsx @@ -40,8 +40,12 @@ export const RightArea = observer(function RightArea({ className }) { const toolsDisabled = appScreenService.rightAreaBottom.getDisplayed({}).length === 0; + function close() { + optionsPanelService.close(); + } + return ( - + @@ -61,7 +65,7 @@ export const RightArea = observer(function RightArea({ className }) { - optionsPanelService.close()} /> + ); diff --git a/webapp/packages/core-blocks/package.json b/webapp/packages/core-blocks/package.json index 8e274afb71..4448edf506 100644 --- a/webapp/packages/core-blocks/package.json +++ b/webapp/packages/core-blocks/package.json @@ -34,6 +34,7 @@ "mobx": "^6", "mobx-react-lite": "^4", "react": "^18", + "react-hotkeys-hook": "^4", "reakit": "^1", "reakit-utils": "^0" }, diff --git a/webapp/packages/core-blocks/src/Slide/SlideBox.tsx b/webapp/packages/core-blocks/src/Slide/SlideBox.tsx index 85d9210ab6..3a0ee17bfc 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideBox.tsx +++ b/webapp/packages/core-blocks/src/Slide/SlideBox.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { useEffect, useRef } from 'react'; -import { s, useS } from '../index.js'; +import { s, useHotkeys, useMergeRefs, useS } from '../index.js'; import SlideBoxStyles from './SlideBox.module.css'; import SlideBoxElementStyles from './SlideElement.module.css'; import SlideBoxOverlayStyles from './SlideOverlay.module.css'; @@ -17,13 +17,17 @@ interface Props { className?: string; children?: React.ReactNode; open?: boolean; + onClose?: () => void; } -export const SlideBox = observer(function SlideBox({ children, open, className }) { +export const SlideBox = observer(function SlideBox({ children, open, className, onClose }) { const slideBoxStyles = useS(SlideBoxStyles); const slideBoxElementStyles = useS(SlideBoxElementStyles); const slideBoxOverlayStyles = useS(SlideBoxOverlayStyles); + const divRef = useRef(null); + const ref = useHotkeys('escape', () => onClose?.(), { enabled: open }); + const mergedRefs = useMergeRefs(ref, divRef); useEffect(() => { const div = divRef.current; @@ -48,7 +52,7 @@ export const SlideBox = observer(function SlideBox({ children, open, clas return (
{children} diff --git a/webapp/packages/core-blocks/src/Slide/SlideElement.module.css b/webapp/packages/core-blocks/src/Slide/SlideElement.module.css index 86b9518fec..d25d7a2f17 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideElement.module.css +++ b/webapp/packages/core-blocks/src/Slide/SlideElement.module.css @@ -11,11 +11,11 @@ display: inline-block; vertical-align: top; white-space: normal; - transition: transform ease-in-out 0.4s; + transition: transform ease-in-out 0.3s; transform: translateX(-100%); &:first-child { - transition: width ease-in-out 0.4s; + transition: width ease-in-out 0.3s; width: 100%; } } diff --git a/webapp/packages/core-blocks/src/Slide/SlideOverlay.module.css b/webapp/packages/core-blocks/src/Slide/SlideOverlay.module.css index e33a01932d..3b1ffd7274 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideOverlay.module.css +++ b/webapp/packages/core-blocks/src/Slide/SlideOverlay.module.css @@ -16,7 +16,7 @@ top: 0; left: 0; background: rgb(0 0 0 / 0%); - transition: background cubic-bezier(0.4, 0, 0.2, 1) 0.6s; + transition: background ease-in-out 0.3s; } .iconBtn { diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index afdb3f7284..78e9c60fad 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -5,6 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +export { useHotkeys } from 'react-hotkeys-hook'; + export * from './CommonDialog/CommonDialog/CommonDialogBody.js'; export * from './CommonDialog/CommonDialog/CommonDialogFooter.js'; export * from './CommonDialog/CommonDialog/CommonDialogHeader.js'; diff --git a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts index 4f88482437..cfb3ad4ecb 100644 --- a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts +++ b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts @@ -186,10 +186,10 @@ export class ConnectionExecutionContextResource extends CachedMapResource - this.values.filter(context => { - const connection = this.connectionInfoResource.get(key); - return context.connectionId === key.connectionId && context.projectId === key.projectId && !connection?.connected; - }), + this.values.filter( + context => + context.connectionId === key.connectionId && context.projectId === key.projectId && !this.connectionInfoResource.isConnected(key), + ), ), ).map(context => context.id), ), diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 6e6ec39cd0..2d1fcd1a99 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -15,6 +15,7 @@ import { CachedMapAllKey, CachedMapResource, type CachedResourceIncludeArgs, + type ICachedResourceMetadata, isResourceAlias, type ResourceKey, resourceKeyList, @@ -76,8 +77,12 @@ export const DEFAULT_NAVIGATOR_VIEW_SETTINGS: NavigatorSettingsInput = { showUtilityObjects: false, }; +export interface IConnectionInfoMetadata extends ICachedResourceMetadata { + connecting?: boolean; +} + @injectable() -export class ConnectionInfoResource extends CachedMapResource { +export class ConnectionInfoResource extends CachedMapResource { readonly onConnectionCreate: ISyncExecutor; readonly onConnectionClose: ISyncExecutor; @@ -237,6 +242,13 @@ export class ConnectionInfoResource extends CachedMapResource): boolean; + isConnecting(key: ResourceKey): boolean; + isConnecting(key: ResourceKey): boolean { + return [this.metadata.get(key)].flat().some(connection => connection?.connecting ?? false); + } + isConnected(key: IConnectionInfoParams): boolean; isConnected(key: ResourceKeyList): boolean; isConnected(key: ResourceKey): boolean; @@ -390,13 +402,19 @@ export class ConnectionInfoResource extends CachedMapResource { - const { connection } = await this.graphQLService.sdk.initConnection({ - ...config, - ...this.getDefaultIncludes(), - ...this.getIncludesMap(key), - }); - this.set(createConnectionParam(connection), connection); - this.onDataOutdated.execute(key); + const metadata = this.metadata.get(key); + metadata.connecting = true; + try { + const { connection } = await this.graphQLService.sdk.initConnection({ + ...config, + ...this.getDefaultIncludes(), + ...this.getIncludesMap(key), + }); + this.set(createConnectionParam(connection), connection); + this.onDataOutdated.execute(key); + } finally { + metadata.connecting = false; + } }); return this.get(key)!; @@ -444,8 +462,10 @@ export class ConnectionInfoResource extends CachedMapResource { + this.set(createConnectionParam(connection), connection); + this.onDataOutdated.execute(key); + }); }); return this.get(key)!; diff --git a/webapp/packages/core-connections/src/ConnectionToolsResource.ts b/webapp/packages/core-connections/src/ConnectionToolsResource.ts index 64d191fb6c..c25aff55e7 100644 --- a/webapp/packages/core-connections/src/ConnectionToolsResource.ts +++ b/webapp/packages/core-connections/src/ConnectionToolsResource.ts @@ -21,7 +21,7 @@ export type ConnectionTools = DatabaseConnectionToolsFragment; export class ConnectionToolsResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, - connectionInfoResource: ConnectionInfoResource, + private readonly connectionInfoResource: ConnectionInfoResource, ) { super(); @@ -44,6 +44,10 @@ export class ConnectionToolsResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, + private readonly navTreeResource: NavTreeResource, private readonly connectionInfoResource: ConnectionInfoResource, appAuthService: AppAuthService, ) { super(); appAuthService.requireAuthentication(this); + this.preloadResource(navTreeResource, key => { + if (isResourceAlias(key)) { + return ''; + } + return ResourceKeyUtils.mapKey( + key, + key => this.connectionInfoResource.get(createConnectionParam(key.projectId, key.connectionId))?.nodePath || '', + ); + }); + this.navTreeResource.onDataOutdated.addHandler(key => { + ResourceKeyUtils.forEach(key, key => { + if (isResourceAlias(key)) { + return; + } + const connection = this.connectionInfoResource.getConnectionForNode(key); + + if (!connection) { + return; + } + + this.markOutdated(resourceKeyList(this.keys.filter(key => key.projectId === connection.projectId && key.connectionId === connection.id))); + }); + }); this.preloadResource(connectionInfoResource, () => ConnectionInfoActiveProjectKey); this.before( ExecutorInterrupter.interrupter(key => { @@ -61,7 +86,7 @@ export class ContainerResource extends CachedMapResource ResourceKeyUtils.forEach(key, key => { - if (!this.connectionInfoResource.get(key)?.connected) { + if (!this.connectionInfoResource.isConnected(key)) { this.delete({ projectId: key.projectId, connectionId: key.connectionId }); } }), diff --git a/webapp/packages/core-connections/src/extensions/IObjectLoaderProvider.ts b/webapp/packages/core-connections/src/extensions/IObjectLoaderProvider.ts new file mode 100644 index 0000000000..8a52b197e6 --- /dev/null +++ b/webapp/packages/core-connections/src/extensions/IObjectLoaderProvider.ts @@ -0,0 +1,21 @@ +/* + * 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 { createExtension, type IExtension, isExtension } from '@cloudbeaver/core-extensions'; +import type { ILoadableState } from '@cloudbeaver/core-utils'; + +const objectLoaderProviderSymbol = Symbol('@extension/ObjectLoaderProvider'); + +export type IObjectLoaderProvider = (context: T) => ILoadableState[]; + +export function objectLoaderProvider(provider: IObjectLoaderProvider) { + return createExtension(provider, objectLoaderProviderSymbol); +} + +export function isObjectLoaderProvider(obj: IExtension): obj is IObjectLoaderProvider & IExtension { + return isExtension(obj, objectLoaderProviderSymbol); +} diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index 7ead664749..83f8e3d3c7 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -16,6 +16,7 @@ export * from './extensions/IObjectCatalogProvider.js'; export * from './extensions/IObjectCatalogSetter.js'; export * from './extensions/IObjectSchemaProvider.js'; export * from './extensions/IObjectSchemaSetter.js'; +export * from './extensions/IObjectLoaderProvider.js'; export * from './extensions/IExecutionContextProvider.js'; export * from './NavTree/ConnectionNavNodeService.js'; export * from './NavTree/NavNodeExtensionsService.js'; diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 827774f152..e51307429e 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -15,6 +15,7 @@ export default [ ['ui_stepper_next', 'Next'], ['ui_stepper_finish', 'Finish'], ['ui_load_more', 'Load more'], + ['ui_processing_connecting', 'Connecting...'], ['ui_processing_loading', 'Loading...'], ['ui_processing_cancel', 'Cancel'], ['ui_processing_canceling', 'Cancelling...'], diff --git a/webapp/packages/core-localization/src/locales/fr.ts b/webapp/packages/core-localization/src/locales/fr.ts index 6c5cc7c945..c03d644e7f 100644 --- a/webapp/packages/core-localization/src/locales/fr.ts +++ b/webapp/packages/core-localization/src/locales/fr.ts @@ -15,6 +15,7 @@ export default [ ['ui_stepper_next', 'Suivant'], ['ui_stepper_finish', 'Terminer'], ['ui_load_more', 'Charger plus'], + ['ui_processing_connecting', 'Connexion en cours...'], ['ui_processing_loading', 'Chargement...'], ['ui_processing_cancel', 'Annuler'], ['ui_processing_canceling', 'Annulation...'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index e773928607..240a2adc28 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -15,6 +15,7 @@ export default [ ['ui_stepper_next', 'Avanti'], ['ui_stepper_finish', 'Termina'], ['ui_load_more', 'Load more'], + ['ui_processing_connecting', 'Collegamento...'], ['ui_processing_loading', 'Caricamento...'], ['ui_processing_cancel', 'Annulla'], ['ui_processing_canceling', 'Annullamento...'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 0e820e2f6f..92cdc87d45 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -13,6 +13,7 @@ export default [ ['ui_dark_theme', 'Темная'], ['ui_stepper_back', 'Назад'], ['ui_load_more', 'Загрузить ещё'], + ['ui_processing_connecting', 'Подключение...'], ['ui_processing_loading', 'Загрузка...'], ['ui_processing_cancel', 'Отменить'], ['ui_processing_canceling', 'Отмена...'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index fd09809a6b..d24c082e92 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -15,6 +15,7 @@ export default [ ['ui_stepper_next', '下一步'], ['ui_stepper_finish', '完成'], ['ui_load_more', 'Load more'], + ['ui_processing_connecting', '连接中...'], ['ui_processing_loading', '加载中...'], ['ui_processing_cancel', '取消'], ['ui_processing_canceling', '取消中...'], diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index 4254026317..fcccdafb0c 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -150,14 +150,16 @@ export class NavTreeResource extends CachedMapResource { - await this.graphQLService.sdk.navRefreshNode({ - nodePath: navNodeId, - }); + this.performUpdate(navNodeId, [], async () => { + await this.graphQLService.sdk.navRefreshNode({ + nodePath: navNodeId, + }); - if (!silent) { - this.markTreeOutdated(navNodeId); - } - await this.onNodeRefresh.execute(navNodeId); + if (!silent) { + this.markOutdated(navNodeId); + } + await this.onNodeRefresh.execute(navNodeId); + }); } markTreeOutdated(navNodeId: ResourceKeySimple): void { diff --git a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx index 8f4d595767..be50f14c97 100644 --- a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx @@ -31,7 +31,7 @@ export const ContextMenu = observer( const menu = useRef(); - useAutoLoad({ name: `${ContextMenu.name}(${menuData.menu.id})` }, menuData.loaders, !lazy, menuVisible); + useAutoLoad({ name: `${ContextMenu.name}(${menuData.menu.id})` }, menuData.loaders, !lazy, menuVisible, true); const handlers = useObjectRef( () => ({ diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx index b5648564d4..a62a86d2a4 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx @@ -58,7 +58,7 @@ export const MenuBar = observer( const mergedRef = useMergeRefs(ref, refNav); const styles = useS(style); const items = menu.items; - useAutoLoad(MenuBar, menu.loaders); + useAutoLoad(MenuBar, menu.loaders, true, false, true); if (!items.length) { return null; @@ -67,7 +67,7 @@ export const MenuBar = observer( return (
- + {items.map(item => ( ))} diff --git a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx index eb81c29de3..836fc997f9 100644 --- a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx @@ -43,7 +43,7 @@ export const SubMenuElement = observer( const handler = subMenuData.handler; const hidden = getComputed(() => handler?.isHidden?.(subMenuData.context)); - useAutoLoad(SubMenuElement, subMenuData.loaders, !hidden, visible); + useAutoLoad(SubMenuElement, subMenuData.loaders, !hidden, visible, true); const handlers = useObjectRef( () => ({ diff --git a/webapp/packages/core-view/package.json b/webapp/packages/core-view/package.json index ba234d189a..5290b1f6b5 100644 --- a/webapp/packages/core-view/package.json +++ b/webapp/packages/core-view/package.json @@ -26,8 +26,7 @@ "@cloudbeaver/core-utils": "^0", "mobx": "^6", "mobx-react-lite": "^4", - "react": "^18", - "react-hotkeys-hook": "^4" + "react": "^18" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-view/src/View/CaptureView.tsx b/webapp/packages/core-view/src/View/CaptureView.tsx index 02c0a17cf8..f675fd2e2d 100644 --- a/webapp/packages/core-view/src/View/CaptureView.tsx +++ b/webapp/packages/core-view/src/View/CaptureView.tsx @@ -7,9 +7,8 @@ */ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { s, useFocus, useS } from '@cloudbeaver/core-blocks'; +import { s, useFocus, useHotkeys, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { isObjectsEqual } from '@cloudbeaver/core-utils'; diff --git a/webapp/packages/plugin-administration/src/Administration/Administration.tsx b/webapp/packages/plugin-administration/src/Administration/Administration.tsx index 1e2798b137..14b075389a 100644 --- a/webapp/packages/plugin-administration/src/Administration/Administration.tsx +++ b/webapp/packages/plugin-administration/src/Administration/Administration.tsx @@ -100,6 +100,10 @@ export const Administration = observer>(function contentRef.current?.scrollTo({ top: 0, left: 0 }); }, [activeScreen?.item]); + function close() { + optionsPanelService.close(); + } + return ( @@ -120,7 +124,7 @@ export const Administration = observer>(function
{children} - +
@@ -132,7 +136,7 @@ export const Administration = observer>(function
- optionsPanelService.close()} /> + diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx index 0a19c4154d..4cf3dcccce 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialogFooter.tsx @@ -27,7 +27,7 @@ export const ConnectionDialogFooter = observer(function ConnectionDialogF {translate('ui_stepper_back')}
); diff --git a/webapp/packages/plugin-connection-template/src/locales/en.ts b/webapp/packages/plugin-connection-template/src/locales/en.ts index cdd315dbc1..baf6d26cd4 100644 --- a/webapp/packages/plugin-connection-template/src/locales/en.ts +++ b/webapp/packages/plugin-connection-template/src/locales/en.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ export default [ - ['plugin_connection_template_connecting', 'Connecting...'], ['plugin_connection_template_connecting_message', 'Connecting to database...'], ['plugin_connection_template_connect_success', 'Connection is established'], ['plugin_connection_template_action_connection_template_label', 'From a Template'], diff --git a/webapp/packages/plugin-connection-template/src/locales/fr.ts b/webapp/packages/plugin-connection-template/src/locales/fr.ts index e8009b5044..2d4df92056 100644 --- a/webapp/packages/plugin-connection-template/src/locales/fr.ts +++ b/webapp/packages/plugin-connection-template/src/locales/fr.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ export default [ - ['plugin_connection_template_connecting', 'Connexion en cours...'], ['plugin_connection_template_connecting_message', 'Connexion à la base de données en cours...'], ['plugin_connection_template_connect_success', 'Connexion établie'], ['plugin_connection_template_action_connection_template_label', "À partir d'un modèle"], diff --git a/webapp/packages/plugin-connection-template/src/locales/it.ts b/webapp/packages/plugin-connection-template/src/locales/it.ts index 4b29b98428..61d0e8c04a 100644 --- a/webapp/packages/plugin-connection-template/src/locales/it.ts +++ b/webapp/packages/plugin-connection-template/src/locales/it.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ export default [ - ['plugin_connection_template_connecting', 'Collegamento...'], ['plugin_connection_template_connecting_message', 'Collegamento al database...'], ['plugin_connection_template_connect_success', 'Connection is established'], ['plugin_connection_template_action_connection_template_label', 'Dal Template'], diff --git a/webapp/packages/plugin-connection-template/src/locales/ru.ts b/webapp/packages/plugin-connection-template/src/locales/ru.ts index 7c4300da47..fae323ebd7 100644 --- a/webapp/packages/plugin-connection-template/src/locales/ru.ts +++ b/webapp/packages/plugin-connection-template/src/locales/ru.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ export default [ - ['plugin_connection_template_connecting', 'Подключение...'], ['plugin_connection_template_connecting_message', 'Подключение к базе...'], ['plugin_connection_template_connect_success', 'Подключение установлено'], ['plugin_connection_template_action_connection_template_label', 'Из шаблона'], diff --git a/webapp/packages/plugin-connection-template/src/locales/zh.ts b/webapp/packages/plugin-connection-template/src/locales/zh.ts index 6b4d9ee3ce..7c344c2adb 100644 --- a/webapp/packages/plugin-connection-template/src/locales/zh.ts +++ b/webapp/packages/plugin-connection-template/src/locales/zh.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ export default [ - ['plugin_connection_template_connecting', '连接中...'], ['plugin_connection_template_connecting_message', '连接数据库...'], ['plugin_connection_template_connect_success', '已建立连接'], ['plugin_connection_template_action_connection_template_label', '从模板创建'], diff --git a/webapp/packages/plugin-connections/src/ConnectionShield.tsx b/webapp/packages/plugin-connections/src/ConnectionShield.tsx index e22dfa5a40..8185e07aeb 100644 --- a/webapp/packages/plugin-connections/src/ConnectionShield.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionShield.tsx @@ -6,9 +6,9 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { type PropsWithChildren, useState } from 'react'; +import { type PropsWithChildren } from 'react'; -import { Button, Loader, TextPlaceholder, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, getComputed, Loader, TextPlaceholder, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -23,28 +23,23 @@ export const ConnectionShield = observer connectionKey && connection.resource.isConnecting(connectionKey)); async function handleConnect() { if (connecting || !connection.data || !connectionKey) { return; } - setConnecting(true); - try { await connectionsManagerService.requireConnection(connectionKey); } catch (exception: any) { notificationService.logException(exception); - } finally { - setConnecting(false); } } - if (connection.data && !connection.data.connected) { - if (connecting || connection.isLoading()) { - return ; + if (getComputed(() => connection.data && !connection.data.connected)) { + if (connecting) { + return ; } return ( diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css index f61bab5f30..abbd9a1564 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css @@ -10,7 +10,6 @@ display: flex; overflow: hidden; box-sizing: border-box; - position: relative; } .container { @@ -28,8 +27,3 @@ display: none; } } - -.cellMenu { - width: 25px; - height: 100%; -} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx index 812c964346..549f032835 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx @@ -59,7 +59,6 @@ export const CellFormatter = observer(function CellFormatter({ className, spreadsheetActions={spreadsheetActions} resultIndex={context.resultIndex} simple={context.simple} - className={s(styles, { cellMenu: true })} onStateSwitch={setMenuVisible} />
diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css index 7ad2141da9..24fd4d4dbf 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css @@ -18,10 +18,15 @@ position: absolute; top: 0px; right: 0px; + height: 100%; } + .menuTrigger { padding: 1px 2px; + display: flex; + align-items: center; height: 100%; + justify-content: center; &:hover { background: none; diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts index 52f365d7e4..c696ef956b 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts @@ -98,6 +98,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { return [ ...this.appAuthService.loaders, + ...this.connectionSchemaManagerService.currentObjectLoaders, getCachedMapResourceLoaderState(this.containerResource, () => ({ ...activeConnectionKey, catalogId: this.connectionSchemaManagerService.activeObjectCatalogId, diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index cfb378c4bf..49078b088d 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -18,6 +18,7 @@ import { type IExecutionContextProvider, type IObjectCatalogProvider, type IObjectCatalogSetter, + type IObjectLoaderProvider, type IObjectSchemaProvider, type IObjectSchemaSetter, isConnectionProvider, @@ -25,6 +26,7 @@ import { isExecutionContextProvider, isObjectCatalogProvider, isObjectCatalogSetter, + isObjectLoaderProvider, isObjectSchemaProvider, isObjectSchemaSetter, type IStructContainers, @@ -44,6 +46,7 @@ import { isProjectSetterState, } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import type { ILoadableState } from '@cloudbeaver/core-utils'; import { type ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; export interface IConnectionInfo { @@ -61,6 +64,7 @@ interface IActiveItem { getCurrentSchemaId?: IObjectSchemaProvider; getCurrentCatalogId?: IObjectCatalogProvider; getCurrentExecutionContext?: IExecutionContextProvider; + getCurrentLoader?: IObjectLoaderProvider; changeConnectionId?: IConnectionSetter; changeProjectId?: IProjectSetter; changeCatalogId?: IObjectCatalogSetter; @@ -132,6 +136,14 @@ export class ConnectionSchemaManagerService { return this.activeItem.getCurrentExecutionContext(this.activeItem.context); } + get currentObjectLoaders(): ILoadableState[] { + if (!this.activeItem?.getCurrentLoader) { + return []; + } + + return this.activeItem.getCurrentLoader(this.activeItem.context); + } + get currentObjectSchemaId(): string | undefined { if (this.pendingSchemaId !== null) { return this.pendingSchemaId; @@ -271,6 +283,7 @@ export class ConnectionSchemaManagerService { currentObjectCatalogId: computed, activeObjectCatalogId: computed, currentObjectSchemaId: computed, + currentObjectLoaders: computed, isConnectionChangeable: computed, isObjectCatalogChangeable: computed, isObjectSchemaChangeable: computed, @@ -456,6 +469,9 @@ export class ConnectionSchemaManagerService { .on(isExecutionContextProvider, extension => { item.getCurrentExecutionContext = extension; }) + .on(isObjectLoaderProvider, extension => { + item.getCurrentLoader = extension; + }) .on(isProjectSetter, extension => { item.changeProjectId = extension; diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx index 3b3b960619..8926de2203 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSelector/ConnectionIcon.tsx @@ -10,6 +10,7 @@ import { observer } from 'mobx-react-lite'; import { ConnectionImageWithMask, ConnectionImageWithMaskSvgStyles, + getComputed, s, SContext, type StyleRegistry, @@ -47,9 +48,16 @@ export const ConnectionIcon = observer(function ConnectionI return null; } - const driver = drivers.resource.get(connection.data.driverId); + const connected = getComputed(() => connection.data?.connected ?? false); + const driverIcon = getComputed(() => { + if (!connection.data?.driverId) { + return null; + } - if (!driver?.icon) { + return drivers.resource.get(connection.data.driverId)?.icon; + }); + + if (!driverIcon) { return null; } @@ -58,8 +66,8 @@ export const ConnectionIcon = observer(function ConnectionI tab.handlerState.tabTitle = data.name; }); }, - active: !connection.isLoading() && connection.data?.connected, + active: getComputed(() => !!connection.tryGetData?.connected), }); const pages = dbObjectPagesService.orderedPages; @@ -64,7 +64,7 @@ export const ObjectViewerPanel: TabHandlerPanelComponent return ( - {node.data ? ( + {node.tryGetData ? ( ) { + // this method must be synchronous with nav-tree update + private removeTabs(key: ResourceKey) { const tabs: string[] = []; - await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey); - ResourceKeyUtils.forEach(key, key => { const tab = this.navigationTabsService.findTab(isObjectViewerTab(tab => tab.handlerState.objectId === key)); @@ -282,9 +281,7 @@ export class ObjectViewerTabService { private getNavNode({ handlerState }: ITab) { if (handlerState.connectionKey) { - const connection = this.connectionInfoResource.get(handlerState.connectionKey); - - if (!connection?.connected) { + if (!this.connectionInfoResource.isConnected(handlerState.connectionKey)) { return; } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index be9c8fdc51..b570968252 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -24,6 +24,7 @@ import { type IConnectionInfoParams, objectCatalogProvider, objectCatalogSetter, + objectLoaderProvider, objectSchemaProvider, objectSchemaSetter, } from '@cloudbeaver/core-connections'; @@ -33,7 +34,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { NavNodeInfoResource, NodeManagerUtils, objectNavNodeProvider } from '@cloudbeaver/core-navigation-tree'; import { projectProvider, projectSetter, projectSetterState } from '@cloudbeaver/core-projects'; -import { resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { getCachedMapResourceLoaderState, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import type { NavNodeInfoFragment } from '@cloudbeaver/core-sdk'; import { isArraysEqual } from '@cloudbeaver/core-utils'; import { type ITab, type ITabOptions, NavigationTabsService, TabHandler } from '@cloudbeaver/plugin-navigation-tabs'; @@ -95,6 +96,7 @@ export class SqlEditorTabService extends Bootstrap { projectProvider(this.getProjectId.bind(this)), connectionProvider(this.getConnectionId.bind(this)), objectCatalogProvider(this.getObjectCatalogId.bind(this)), + objectLoaderProvider(this.getObjectLoader.bind(this)), objectSchemaProvider(this.getObjectSchemaId.bind(this)), executionContextProvider(this.getExecutionContext.bind(this)), projectSetter(this.setProjectId.bind(this)), @@ -182,9 +184,7 @@ export class SqlEditorTabService extends Bootstrap { const { projectId, connectionId, defaultCatalog, defaultSchema } = executionContext; const connectionKey = createConnectionParam(projectId, connectionId); - const connection = this.connectionInfoResource.get(connectionKey); - - if (!connection?.connected) { + if (!this.connectionInfoResource.isConnected(connectionKey)) { return; } @@ -209,8 +209,6 @@ export class SqlEditorTabService extends Bootstrap { const parents = this.navNodeInfoResource.getParents(nodeId); - untracked(() => this.navNodeInfoResource.load(nodeId!)); - return { nodeId, path: parents, @@ -327,6 +325,34 @@ export class SqlEditorTabService extends Bootstrap { return createConnectionParam(context.projectId, context.connectionId); } + private getObjectLoader(tab: ITab) { + const executionContextComputed = computed(() => this.sqlDataSourceService.get(tab.handlerState.editorId)?.executionContext); + + const connectionKeyComputed = computed(() => { + const executionContext = executionContextComputed.get(); + + if (!executionContext) { + return null; + } + + return createConnectionParam(executionContext.projectId, executionContext.connectionId); + }); + + return [ + getCachedMapResourceLoaderState(this.connectionInfoResource, () => connectionKeyComputed.get()), + getCachedMapResourceLoaderState(this.connectionExecutionContextResource, () => executionContextComputed.get()?.id || null), + getCachedMapResourceLoaderState(this.containerResource, () => connectionKeyComputed.get()), + // TODO: maybe we need it for this.getNavNode to work properly, but it's seems working without it + // getCachedMapResourceLoaderState(this.navNodeInfoResource, () => { + // if (this.containerResource.isLoadable(connectionKey)) { + // return null; + // } + // console.log('node:', this.getNavNode(tab)?.nodeId || null); + // return this.getNavNode(tab)?.nodeId || null; + // }), + ]; + } + private getObjectCatalogId(tab: ITab) { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); const context = this.connectionExecutionContextResource.get(dataSource?.executionContext?.id ?? ''); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx index cc75c0331e..4d176e880f 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorOverlay.tsx @@ -94,7 +94,7 @@ export const SqlEditorOverlay = observer(function SqlEditorOverlay({ stat }, [connected, initExecutionContext]); return ( - + {connection.tryGetData?.name} diff --git a/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.module.css b/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.module.css index 760eef637a..380f8422e0 100644 --- a/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.module.css +++ b/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.module.css @@ -14,3 +14,7 @@ display: none; } } + +.appStateMenu > .loader { + height: 100%; +} diff --git a/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.tsx b/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.tsx index 6189b0fc14..b60c5f329d 100644 --- a/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.tsx +++ b/webapp/packages/plugin-top-app-bar/src/TopNavBar/AppStateMenu/AppStateMenu.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { AppAuthService } from '@cloudbeaver/core-authentication'; -import { s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; +import { Loader, s, SContext, type StyleRegistry, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { MenuBar, MenuBarItemStyles, MenuBarStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -46,7 +46,9 @@ export const AppStateMenu = observer(function AppStateMenu() { return (
- + + +
); diff --git a/webapp/packages/plugin-top-app-bar/src/TopNavBar/MainMenu/MainMenu.tsx b/webapp/packages/plugin-top-app-bar/src/TopNavBar/MainMenu/MainMenu.tsx index 80a04b5efe..582b638cce 100644 --- a/webapp/packages/plugin-top-app-bar/src/TopNavBar/MainMenu/MainMenu.tsx +++ b/webapp/packages/plugin-top-app-bar/src/TopNavBar/MainMenu/MainMenu.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { AppAuthService } from '@cloudbeaver/core-authentication'; -import { s, useS } from '@cloudbeaver/core-blocks'; +import { Loader, s, useS } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { MenuBar } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; @@ -27,7 +27,9 @@ export const MainMenu = observer(function MainMenu() { return (
- + + +
); }); diff --git a/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/TopMenuWrapper.module.css b/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/TopMenuWrapper.module.css index 5b363f1b79..1600ffc0c9 100644 --- a/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/TopMenuWrapper.module.css +++ b/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/TopMenuWrapper.module.css @@ -8,4 +8,8 @@ .menuWrapper { display: flex; height: 100%; + + &.loader { + height: 100%; + } }