From f5a2799ec94be266478e48296bef74dd1a82f41d Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 22 Dec 2022 14:04:03 +0300 Subject: [PATCH] CB-2912 refactor: use websocket (#1348) * CB-2912 refactor: use websocket * CB-2912 chore: disable websocket by default * CB-2912 feat: websocket events * CB-2912 feat: websocket protocol * CB-2912 fix: gql schema * CB-2912 feat: update sdk * CB-2912 chore: remove deprecated code * CB-2912 feat: dynamic events subscription * CB-2912 fix: container resource api * CB-2923 test websocket * CB-2923 refactor event model * CB-2923 dynamic ws event deserializer * CB-2912 chore: update schema * CB-2923 separate events for create/delete/update action * CB-2923 manifest fix * CB-2923 update qql schema * CB-2923 update eclipse configuration * CB-2912 feat: update websocket api * CB-2923 update client event model * CB-2912 feat: resource connection * CB-2923 support new event types * CB-2912 chore: update schema * CB-2923 websocket ping pong * CB-2912 fix: rename topic to topicId in WebSocketClientEvent * CB-2923 send event if datasource description was changed * CB-2923 split WebSession * CB-2912 fix: resources connection * CB-2912 websocket client (#1370) * CB-2989 create ws client * CB-2912 refactor: rename useResource * CB-2912 resolve conflicts * CB-2912 fix: use wss with https protocol Co-authored-by: Aleksandr Skoblikov --- .../io.cloudbeaver.model/META-INF/MANIFEST.MF | 3 +- .../bundles/io.cloudbeaver.model/plugin.xml | 1 - .../schema/io.cloudbeaver.event.handler.exsd | 11 - .../src/io/cloudbeaver/events/CBEvent.java | 63 ----- .../cloudbeaver/events/CBEventController.java | 86 ------ .../registry/CBEventHandlerDescriptor.java | 44 --- .../registry/CBEventHandlersRegistry.java | 61 ----- .../model/app/BaseWebApplication.java | 4 +- .../cloudbeaver/model/app/WebApplication.java | 4 +- .../model/session/BaseWebSession.java | 161 +++++++++++ .../model/session/WebHeadlessSession.java | 44 +++ .../cloudbeaver/model/session/WebSession.java | 115 ++------ .../session/WebSessionAuthProcessor.java | 2 +- .../model/session/WebUserContext.java | 54 +++- .../io/cloudbeaver/utils/WebEventUtils.java | 163 +++++++----- .../CBWebSessionEventHandler.java} | 21 +- .../META-INF/MANIFEST.MF | 1 + .../bundles/io.cloudbeaver.server/plugin.xml | 10 +- .../schema/service.core.graphqls | 25 -- .../schema/service.events.graphqls | 100 +++++++ .../io/cloudbeaver/server/CBApplication.java | 11 +- .../CBDataSourceUpdatedEventHandlerImpl.java | 52 ---- .../CBFolderUpdatedEventHandlerImpl.java | 52 ---- ...> WSConfigurationChangedEventHandler.java} | 25 +- .../WSDataSourceUpdatedEventHandlerImpl.java | 63 +++++ .../WSFolderUpdatedEventHandlerImpl.java | 57 ++++ ...java => WSProjectUpdatedEventHandler.java} | 22 +- ... WSRmResourceUpdatedEventHandlerImpl.java} | 55 ++-- .../server/jetty/CBJettyServer.java | 33 ++- .../server/websockets/CBEventsWebSocket.java | 117 ++++++++ .../websockets/CBJettyWebSocketManager.java | 101 +++++++ .../websockets/WebSocketPingPongJob.java | 59 +++++ .../service/core/DBWServiceCore.java | 4 - .../service/core/WebServiceBindingCore.java | 3 - .../service/core/impl/WebServiceCore.java | 52 ++-- .../navigator/impl/WebServiceNavigator.java | 16 +- .../session/SMTokenCredentialProvider.java} | 31 +-- .../service/session/WebSessionManager.java | 81 ++++-- .../service/rm/impl/WebServiceRM.java | 27 +- .../CloudbeaverServer.product.launch | 3 +- .../META-INF/MANIFEST.MF | 1 + .../ConfigurationWizardScreen.tsx | 4 +- webapp/packages/core-app/src/Body.tsx | 9 +- .../src/AuthConfigurationsResource.ts | 2 +- .../src/UserInfoResource.ts | 5 +- .../useResource.ts} | 214 ++++++++------- webapp/packages/core-blocks/src/index.ts | 3 +- .../core-blocks/src/useDataResource.ts | 226 ---------------- .../core-blocks/src/usePromiseState.ts | 5 +- .../configs/webpack.product.dev.config.js | 123 +++++---- .../src/ConnectionFolderEventHandler.ts | 25 +- .../src/ConnectionInfoEventHandler.ts | 25 +- .../src/ConnectionInfoResource.ts | 35 ++- .../core-connections/src/ContainerResource.ts | 13 +- .../core-connections/src/DBDriverResource.ts | 2 +- .../src/NavTree/ConnectionNavNodeService.ts | 34 ++- .../src/NodesManager/DBObjectResource.ts | 7 +- .../src/NodesManager/NavNodeInfoResource.ts | 15 +- .../src/ProjectInfoEventHandler.ts | 44 +++ .../core-projects/src/ProjectInfoResource.ts | 2 +- .../core-projects/src/ProjectsService.ts | 29 +- webapp/packages/core-projects/src/manifest.ts | 2 + .../src/ResourceManagerEventHandler.ts | 25 +- .../src/ResourceManagerResource.ts | 32 ++- .../src/SharedProjectsResource.ts | 2 +- webapp/packages/core-root/package.json | 5 +- .../core-root/src/ServerConfigEventHandler.ts | 22 +- .../core-root/src/ServerConfigResource.ts | 4 +- .../ServerEventEmitter/IServerEventEmitter.ts | 47 ++++ .../ServerEventEmitter/TopicEventHandler.ts | 197 ++++++++++++++ .../core-root/src/SessionEventSource.ts | 148 ++++++++++- .../src/SessionPermissionsResource.ts | 2 +- .../core-root/src/getSocketIterable.ts | 97 +++++++ webapp/packages/core-root/src/index.ts | 2 + webapp/packages/core-sdk/codegen.yml | 1 + .../core-sdk/src/EnvironmentService.ts | 1 + .../src/Resource/CachedDataResource.ts | 39 +-- .../src/Resource/CachedMapResource.ts | 106 +++++--- .../core-sdk/src/Resource/CachedResource.ts | 250 +++++++++++++----- .../src/Resource/CachedResourceIncludes.ts | 17 +- .../core-sdk/src/Resource/EventSource.ts | 109 -------- .../src/Resource/ResourceEventHandler.ts | 66 ----- .../core-sdk/src/Resource/ResourceKeyList.ts | 1 + webapp/packages/core-sdk/src/ServiceError.ts | 21 ++ webapp/packages/core-sdk/src/index.ts | 3 +- .../src/queries/session/getSessionEvents.gql | 10 - webapp/packages/core-sdk/src/sdk.ts | 99 ++++--- .../core-utils/src/GlobalConstants.ts | 16 ++ .../packages/core-utils/src/ILoadableState.ts | 1 + webapp/packages/core-utils/src/underscore.ts | 2 +- .../Form/ServerConfigurationFeaturesForm.tsx | 4 +- .../ServerConfigurationDriversForm.tsx | 4 +- .../AuthConfiguration.tsx | 4 +- .../Options/AuthConfigurationOptions.tsx | 6 +- .../AuthenticationProviders.tsx | 4 +- .../Form/UserInfo/MetaParameterInfoPanel.tsx | 4 +- .../Users/MetaParameters/MetaParameters.tsx | 4 +- .../GrantedConnections/GrantedConnections.tsx | 8 +- .../Users/Teams/GrantedUsers/GrantedUsers.tsx | 4 +- .../Users/Teams/Options/Permissions.tsx | 4 +- .../Users/Teams/Options/TeamOptions.tsx | 6 +- .../Users/Teams/TeamsTable/useTeamsTable.tsx | 4 +- .../Users/UserForm/UserInfo.tsx | 6 +- .../UsersTable/Filters/UsersTableFilters.tsx | 4 +- .../Users/UsersTable/UsersTable.tsx | 10 +- .../src/Dialog/useAuthDialogState.ts | 4 +- .../src/Search/SearchDatabase.tsx | 6 +- .../Connections/ConnectionsAdministration.tsx | 4 +- .../ConnectionDetailsInfo/SSH.tsx | 4 +- .../CreateConnection/CreateConnection.tsx | 6 +- .../Manual/CustomConnection.tsx | 6 +- .../ConnectionAccess/ConnectionAccess.tsx | 6 +- .../useConnectionAccessState.ts | 5 +- .../ConnectionAuthenticationForm.tsx | 5 +- .../NetworkHandlerAuthForm.tsx | 4 +- .../ConnectionFormBaseActions.tsx | 7 +- .../DriverProperties/DriverProperties.tsx | 7 +- .../src/ConnectionForm/Options/Options.tsx | 14 +- .../ConnectionFormAuthenticationAction.tsx | 11 +- .../ConnectionForm/OriginInfo/OriginInfo.tsx | 8 +- .../src/ConnectionForm/SSH/SSH.tsx | 4 +- .../src/ConnectionForm/SSH/SSHTab.tsx | 4 +- .../src/Dialog/OutputOptionsForm.tsx | 4 +- .../useDataViewerDatabaseDataModel.ts | 4 +- .../ConnectionSelector/ConnectionIcon.tsx | 6 +- .../plugin-devtools/src/PluginBootstrap.ts | 3 +- .../ElementsTree/ElementsTree.tsx | 4 +- .../ElementsTreeTools/ElementsTreeTools.tsx | 4 +- .../ElementsTree/useElementsTree.ts | 4 +- .../NavigationNodeProjectControl.tsx | 2 +- .../NavNodeMetadata/ObjectProperties.tsx | 4 +- .../VirtualFolder/VirtualFolderPanel.tsx | 6 +- .../ObjectPropertiesPage/ObjectFolders.tsx | 4 +- .../ObjectPropertyTable.tsx | 6 +- .../src/ObjectViewerPanel.tsx | 8 +- .../plugin-projects/src/ProjectSelect.tsx | 4 +- .../src/Tree/ResourceManagerTree.tsx | 6 +- .../src/ResourceSqlDataSource.ts | 2 +- .../src/Screen/SqlEditorScreen.tsx | 4 +- .../src/ScriptPreview/ScriptPreviewDialog.tsx | 4 +- .../src/SqlDataSource/BaseSqlDataSource.ts | 5 + .../plugin-sql-editor/src/SqlEditor.tsx | 8 +- .../AuthProviders/AuthProvidersList.tsx | 4 +- .../AuthTokens/AuthTokenList.tsx | 4 +- .../UserInfo/UserInfoPanel.tsx | 4 +- .../src/UserProfileForm/UserProfileForm.tsx | 9 +- .../src/VersionInfo.tsx | 4 +- .../src/VersionUpdate.tsx | 4 +- .../src/VersionUpdateDrawerItem.tsx | 4 +- webapp/yarn.lock | 7 + 150 files changed, 2521 insertions(+), 1779 deletions(-) delete mode 100644 server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd delete mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java delete mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java delete mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java delete mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java rename server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/{events/CBEventHandler.java => websocket/CBWebSessionEventHandler.java} (67%) create mode 100644 server/bundles/io.cloudbeaver.server/schema/service.events.graphqls delete mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java delete mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBFolderUpdatedEventHandlerImpl.java rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/{CBConfigurationChangedEventHandler.java => WSConfigurationChangedEventHandler.java} (62%) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/{CBProjectUpdatedEventHandler.java => WSProjectUpdatedEventHandler.java} (62%) rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/{CBRmResourceUpdatedEventHandlerImpl.java => WSRmResourceUpdatedEventHandlerImpl.java} (56%) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java rename server/bundles/{io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java => io.cloudbeaver.server/src/io/cloudbeaver/service/session/SMTokenCredentialProvider.java} (53%) rename webapp/packages/core-blocks/src/{useMapResource.ts => ResourcesHooks/useResource.ts} (61%) delete mode 100644 webapp/packages/core-blocks/src/useDataResource.ts create mode 100644 webapp/packages/core-projects/src/ProjectInfoEventHandler.ts create mode 100644 webapp/packages/core-root/src/ServerEventEmitter/IServerEventEmitter.ts create mode 100644 webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts create mode 100644 webapp/packages/core-root/src/getSocketIterable.ts delete mode 100644 webapp/packages/core-sdk/src/Resource/EventSource.ts delete mode 100644 webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts create mode 100644 webapp/packages/core-sdk/src/ServiceError.ts delete mode 100644 webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index e6487d7179..52d8a91f91 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -10,6 +10,7 @@ Bundle-ActivationPolicy: lazy Bundle-ClassPath: . Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport, org.jkiss.dbeaver.model.sm;visibility:=reexport, + org.jkiss.dbeaver.model.event;visibility:=reexport, org.jkiss.dbeaver.registry;visibility:=reexport, org.jkiss.dbeaver.net.ssh, org.jkiss.bundle.graphql.java;visibility:=reexport, @@ -20,7 +21,7 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.auth, io.cloudbeaver.auth.provider, io.cloudbeaver.auth.provider.local, - io.cloudbeaver.events, + io.cloudbeaver.websocket, io.cloudbeaver.model, io.cloudbeaver.model.app, io.cloudbeaver.model.rm, diff --git a/server/bundles/io.cloudbeaver.model/plugin.xml b/server/bundles/io.cloudbeaver.model/plugin.xml index 69dc8712a8..b6ba6b0614 100644 --- a/server/bundles/io.cloudbeaver.model/plugin.xml +++ b/server/bundles/io.cloudbeaver.model/plugin.xml @@ -6,7 +6,6 @@ - diff --git a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd b/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd deleted file mode 100644 index 93269606b0..0000000000 --- a/server/bundles/io.cloudbeaver.model/schema/io.cloudbeaver.event.handler.exsd +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - Web events handler - - - diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java deleted file mode 100644 index 4b2ccff4ef..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEvent.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 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.events; - -import org.jkiss.code.NotNull; -import org.jkiss.code.Nullable; - -import java.util.Map; - -/** - * CloudBeaver event - */ -public class CBEvent { - @Nullable - private final String sessionId; - @NotNull - private final String eventType; - @NotNull - private final Map eventData; - - public CBEvent(@NotNull String eventType) { - this(eventType, null, Map.of()); - } - - public CBEvent(@NotNull String eventType, @Nullable String sessionId) { - this(eventType, sessionId, Map.of()); - } - - public CBEvent(@NotNull String eventType, @Nullable String sessionId, @NotNull Map eventData) { - this.eventType = eventType; - this.sessionId = sessionId; - this.eventData = eventData; - } - - @NotNull - public String getEventType() { - return eventType; - } - - @Nullable - public String getSessionId() { - return sessionId; - } - - @NotNull - public Map getEventData() { - return eventData; - } -} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java deleted file mode 100644 index 011d83c75f..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventController.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 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.events; - -import io.cloudbeaver.events.registry.CBEventHandlersRegistry; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.runtime.AbstractJob; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class CBEventController { - private final Map> eventHandlersByType = new HashMap<>(); - - public CBEventController() { - var eventHandlers = CBEventHandlersRegistry.getInstance().getEventHandlers(); - - eventHandlers - .forEach(handler -> eventHandlersByType.computeIfAbsent(handler.getSupportedEventType(), x -> new ArrayList<>()).add(handler)); - } - - private final List eventsPool = new ArrayList<>(); - - /** - * Add cb event to the event pool - */ - public void addEvent(@NotNull CBEvent event) { - synchronized (eventsPool) { - eventsPool.add(event); - } - } - - /** - * Add cb event to the event pool - */ - public void scheduleCheckJob() { - new CBEventCheckJob().schedule(); - } - - private class CBEventCheckJob extends AbstractJob { - private static final long CHECK_PERIOD = 1000; - - protected CBEventCheckJob() { - super("CloudBeaver events job"); - } - - @Override - protected IStatus run(DBRProgressMonitor monitor) { - List events; - - synchronized (eventsPool) { - events = List.copyOf(eventsPool); - eventsPool.clear(); - } - if (events.isEmpty()) { - schedule(CHECK_PERIOD); - return Status.OK_STATUS; - } - for (CBEvent event : events) { - eventHandlersByType.getOrDefault(event.getEventType(), List.of()) - .forEach(handler -> handler.handleEvent(event)); - } - schedule(CHECK_PERIOD); - return Status.OK_STATUS; - } - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java deleted file mode 100644 index 65fad36ee6..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlerDescriptor.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 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.events.registry; - -import io.cloudbeaver.events.CBEventHandler; -import org.eclipse.core.runtime.IConfigurationElement; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.impl.AbstractDescriptor; - -/** - * CB event handler descriptor - */ -public class CBEventHandlerDescriptor extends AbstractDescriptor { - private final ObjectType implType; - - protected CBEventHandlerDescriptor(IConfigurationElement contributorConfig) { - super(contributorConfig); - this.implType = new ObjectType(contributorConfig, "class"); - } - - @NotNull - public CBEventHandler getInstance() { - try { - return implType.createInstance(CBEventHandler.class); - } catch (DBException e) { - throw new IllegalStateException("Can not instantiate event handler '" + implType.getImplName() + "'", e); - } - } -} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java deleted file mode 100644 index 6cd82eb415..0000000000 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/registry/CBEventHandlersRegistry.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 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.events.registry; - -import io.cloudbeaver.events.CBEventHandler; -import org.eclipse.core.runtime.IConfigurationElement; -import org.eclipse.core.runtime.Platform; -import org.jkiss.code.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -public class CBEventHandlersRegistry { - private static final String EXTENSION_ID = "io.cloudbeaver.event.handler"; - private static final String EVENT_HANDLER_TAG = "eventHandler"; - private static CBEventHandlersRegistry instance = null; - - public synchronized static CBEventHandlersRegistry getInstance() { - if (instance == null) { - instance = new CBEventHandlersRegistry(); - } - return instance; - } - - @NotNull - public List getEventHandlers() { - List eventHandlerDescriptors = readDescriptors(); - - return eventHandlerDescriptors.stream() - .map(CBEventHandlerDescriptor::getInstance) - .collect(Collectors.toList()); - } - - @NotNull - private List readDescriptors() { - var result = new ArrayList(); - var registry = Platform.getExtensionRegistry(); - for (IConfigurationElement ext : registry.getConfigurationElementsFor(EXTENSION_ID)) { - // Load webServices - if (EVENT_HANDLER_TAG.equals(ext.getName())) { - result.add(new CBEventHandlerDescriptor(ext)); - } - } - return result; - } -} 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 3099bbcd1a..599242c063 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 @@ -18,7 +18,6 @@ package io.cloudbeaver.model.app; import io.cloudbeaver.DataSourceFilter; import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.events.CBEventController; import io.cloudbeaver.model.log.SLF4JLogHandler; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.WebWorkspace; @@ -37,6 +36,7 @@ import org.jkiss.dbeaver.model.impl.app.ApplicationRegistry; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.websocket.event.WSEventController; import org.jkiss.dbeaver.registry.BaseApplicationImpl; import org.jkiss.dbeaver.runtime.IVariableResolver; import org.jkiss.dbeaver.utils.GeneralUtils; @@ -207,7 +207,7 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements } @Override - public CBEventController getEventController() { + public WSEventController getEventController() { return null; } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index a2f57c0edf..aae436c828 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -18,7 +18,6 @@ package io.cloudbeaver.model.app; import io.cloudbeaver.DataSourceFilter; import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.events.CBEventController; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; @@ -29,6 +28,7 @@ import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.dbeaver.model.websocket.event.WSEventController; import java.nio.file.Path; @@ -73,7 +73,7 @@ public interface WebApplication extends DBPApplication { String getApplicationInstanceId() throws DBException; - CBEventController getEventController(); + WSEventController getEventController(); /** * Port this server listens on diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java new file mode 100644 index 0000000000..4d76dd270f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -0,0 +1,161 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.model.session; + +import io.cloudbeaver.model.app.WebApplication; +import io.cloudbeaver.websocket.CBWebSessionEventHandler; +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.SMAuthSpace; +import org.jkiss.dbeaver.model.auth.SMSessionContext; +import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent; +import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.runtime.DBWorkbench; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Base CB web session + */ +public abstract class BaseWebSession extends AbstractSessionPersistent { + private static final Log log = Log.getLog(BaseWebSession.class); + + protected final SessionContextImpl sessionAuthContext; + @NotNull + protected final String id; + protected final long createTime; + @NotNull + protected final WebUserContext userContext; + @NotNull + protected final WebApplication application; + protected volatile long lastAccessTime; + + private final List sessionEventHandlers = new ArrayList<>(); + private final Set subscribedEventTopics = new CopyOnWriteArraySet<>(); + + public BaseWebSession(@NotNull String id, @NotNull WebApplication application) throws DBException { + this.id = id; + this.application = application; + this.createTime = System.currentTimeMillis(); + this.lastAccessTime = this.createTime; + this.sessionAuthContext = new SessionContextImpl(null); + this.sessionAuthContext.addSession(this); + this.userContext = new WebUserContext(this.application); + } + + public void addSessionEvent(WSEvent event) { + boolean eventAllowedByFilter = subscribedEventTopics.isEmpty() || subscribedEventTopics.contains(event.getTopicId()); + if (!eventAllowedByFilter) { + return; + } + synchronized (sessionEventHandlers) { + for (CBWebSessionEventHandler eventHandler : sessionEventHandlers) { + try { + eventHandler.handeWebSessionEvent(event); + } catch (DBException e) { + log.error(e.getMessage(), e); + addSessionError(e); + } + } + } + } + + public abstract void addSessionError(Throwable exception); + + public void addEventHandler(@NotNull CBWebSessionEventHandler handler) { + synchronized (sessionEventHandlers) { + sessionEventHandlers.add(handler); + } + } + + public void removeEventHandler(@NotNull CBWebSessionEventHandler handler) { + synchronized (sessionEventHandlers) { + sessionEventHandlers.remove(handler); + } + } + + public synchronized boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException { + return userContext.refresh(smAuthInfo); + } + + public void subscribeOnEventTopic(@NotNull String topic) { + subscribedEventTopics.add(topic); + } + + public void unsubscribeFromEventTopic(@NotNull String topic) { + subscribedEventTopics.remove(topic); + } + + @NotNull + @Override + public SMAuthSpace getSessionSpace() { + return DBWorkbench.getPlatform().getWorkspace(); + } + + @NotNull + @Override + public SMSessionContext getSessionContext() { + return sessionAuthContext; + } + + @NotNull + @Property + public String getSessionId() { + return id; + } + + @NotNull + @Override + public LocalDateTime getSessionStart() { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault()); + } + + public long getLastAccessTimeMillis() { + return lastAccessTime; + } + + public synchronized void touchSession() { + this.lastAccessTime = System.currentTimeMillis(); + } + + @NotNull + public synchronized WebUserContext getUserContext() { + return userContext; + } + + @Override + public void close() { + super.close(); + for (CBWebSessionEventHandler sessionEventHandler : sessionEventHandlers) { + sessionEventHandler.close(); + } + } + + public boolean isProjectAccessible(String projectId) { + return userContext.getAccessibleProjectIds().contains(projectId); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java new file mode 100644 index 0000000000..a0127d1830 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebHeadlessSession.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.model.session; + +import io.cloudbeaver.model.app.WebApplication; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.auth.SMSessionPrincipal; + +/** + * Headless CB web session + */ +public class WebHeadlessSession extends BaseWebSession { + public WebHeadlessSession( + @NotNull String id, + @NotNull WebApplication application + ) throws DBException { + super(id, application); + } + + @Override + public void addSessionError(Throwable exception) { + + } + + @Override + public SMSessionPrincipal getSessionPrincipal() { + return null; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index d0803aeeda..73ff2f1aed 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -20,8 +20,6 @@ import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; import io.cloudbeaver.DataSourceFilter; import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.WebServerMessage; @@ -44,27 +42,27 @@ import org.jkiss.dbeaver.model.access.DBACredentialsProvider; import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.auth.*; -import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative; import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials; -import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl; import org.jkiss.dbeaver.model.meta.Association; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; -import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.BaseProgressMonitor; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.ProxyProgressMonitor; -import org.jkiss.dbeaver.model.security.*; +import org.jkiss.dbeaver.model.security.SMAdminController; +import org.jkiss.dbeaver.model.security.SMConstants; +import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.dbeaver.model.security.SMObjects; import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; import org.jkiss.dbeaver.model.sql.DBQuotaException; -import org.jkiss.dbeaver.registry.BaseProjectImpl; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; @@ -73,9 +71,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.lang.reflect.InvocationTargetException; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -85,7 +80,7 @@ import java.util.stream.Collectors; * Web session. * Is the main source of data in web application */ -public class WebSession extends AbstractSessionPersistent +public class WebSession extends BaseWebSession implements SMSession, SMCredentialsProvider, DBACredentialsProvider, IAdaptable { private static final Log log = Log.getLog(WebSession.class); @@ -98,9 +93,6 @@ public class WebSession extends AbstractSessionPersistent private final AtomicInteger taskCount = new AtomicInteger(); - private final String id; - private final long createTime; - private volatile long lastAccessTime; private long maxSessionIdleTime; private String lastRemoteAddr; private String lastRemoteUserAgent; @@ -123,13 +115,7 @@ public class WebSession extends AbstractSessionPersistent private final DBRProgressMonitor progressMonitor = new SessionProgressMonitor(); private WebProjectImpl defaultProject; private final List accessibleProjects = new ArrayList<>(); - private final SessionContextImpl sessionAuthContext; - private final WebApplication application; private final Map sessionHandlers; - private final WebUserContext userContext; - private final List sessionEvents = new ArrayList<>(); - - private RMController rmController; public WebSession( @NotNull HttpSession httpSession, @@ -137,23 +123,11 @@ public class WebSession extends AbstractSessionPersistent @NotNull Map sessionHandlers, long maxSessionIdleTime ) throws DBException { - this.id = httpSession.getId(); - this.createTime = System.currentTimeMillis(); + super(httpSession.getId(), application); this.lastAccessTime = this.createTime; this.locale = CommonUtils.toString(httpSession.getAttribute(ATTR_LOCALE), this.locale); - this.sessionAuthContext = new SessionContextImpl(null); - this.sessionAuthContext.addSession(this); - this.application = application; this.sessionHandlers = sessionHandlers; - this.userContext = new WebUserContext(application); this.maxSessionIdleTime = maxSessionIdleTime; - this.rmController = application.createResourceController(this); - } - - @NotNull - @Override - public SMAuthSpace getSessionSpace() { - return DBWorkbench.getPlatform().getWorkspace(); } @Override @@ -166,18 +140,6 @@ public class WebSession extends AbstractSessionPersistent } } - @NotNull - @Property - public String getSessionId() { - return id; - } - - @NotNull - @Override - public LocalDateTime getSessionStart() { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault()); - } - public WebApplication getApplication() { return application; } @@ -187,11 +149,6 @@ public class WebSession extends AbstractSessionPersistent return defaultProject; } - @NotNull - public SMSessionContext getSessionContext() { - return sessionAuthContext; - } - @Property public String getCreateTime() { return CBModelConstants.ISO_DATE_FORMAT.format(createTime); @@ -202,10 +159,6 @@ public class WebSession extends AbstractSessionPersistent return CBModelConstants.ISO_DATE_FORMAT.format(lastAccessTime); } - public long getLastAccessTimeMillis() { - return lastAccessTime; - } - public String getLastRemoteAddr() { return lastRemoteAddr; } @@ -240,10 +193,6 @@ public class WebSession extends AbstractSessionPersistent return allMetaParams; } - public synchronized WebUserContext getUserContext() { - return userContext; - } - public synchronized String getUserId() { return userContext.getUserId(); } @@ -278,7 +227,7 @@ public class WebSession extends AbstractSessionPersistent } public synchronized RMController getRmController() { - return rmController; + return userContext.getRmController(); } public synchronized void refreshUserData() { @@ -290,16 +239,20 @@ public class WebSession extends AbstractSessionPersistent /** * updates connections based on event in web session * - * @param project project of connection + * @param project project of connection * @param connectionIds list of updated connections - * @param type type of event + * @param type type of event */ - public synchronized void updateProjectConnection(DBPProject project, List connectionIds, CBEventConstants.EventType type) { + public synchronized void updateProjectConnection( + DBPProject project, + List connectionIds, + WSEventType type + ) { DBPDataSourceRegistry registry = project.getDataSourceRegistry(); registry.refreshConfig(); for (String connectionId : connectionIds) { switch (type) { - case TYPE_CREATE: + case DATASOURCE_CREATED: DBPDataSourceContainer container = registry.getDataSource(connectionId); if (container == null) { break; @@ -307,7 +260,7 @@ public class WebSession extends AbstractSessionPersistent WebConnectionInfo connectionInfo = new WebConnectionInfo(this, registry.getDataSource(connectionId)); this.connections.put(connectionInfo.getId(), connectionInfo); break; - case TYPE_DELETE: + case DATASOURCE_DELETED: this.connections.remove(connectionId); break; default: @@ -502,9 +455,6 @@ public class WebSession extends AbstractSessionPersistent addSessionError(e); log.error("Error reading session permissions", e); } - - // Initialize new resource controller - this.rmController = application.createResourceController(this); } private synchronized void refreshAccessibleConnectionIds() { @@ -516,7 +466,7 @@ public class WebSession extends AbstractSessionPersistent return; } SMAuthInfo authInfo = getSecurityController().authenticateAnonymousUser(this.id, getSessionParameters(), CB_SESSION_TYPE); - updateSMAuthInfo(authInfo); + updateSMSession(authInfo); } @NotNull @@ -549,8 +499,8 @@ public class WebSession extends AbstractSessionPersistent HttpServletResponse response, long maxSessionIdleTime ) throws DBWebException { + touchSession(); HttpSession httpSession = request.getSession(); - this.lastAccessTime = System.currentTimeMillis(); this.lastRemoteAddr = request.getRemoteAddr(); this.lastRemoteUserAgent = request.getHeader("User-Agent"); this.maxSessionIdleTime = maxSessionIdleTime; @@ -641,6 +591,7 @@ public class WebSession extends AbstractSessionPersistent this.defaultProject.dispose(); this.defaultProject = null; } + super.close(); } @@ -758,27 +709,6 @@ public class WebSession extends AbstractSessionPersistent addSessionMessage(new WebServerMessage(WebServerMessage.MessageType.INFO, message)); } - public void addSessionEvent(CBEvent event) { - synchronized (sessionEvents) { - sessionEvents.add(event); - } - } - - public List getSessionEvents(int eventsCount) { - synchronized (sessionEvents) { - List result; - if (sessionEvents.size() <= eventsCount) { - result = List.copyOf(sessionEvents); - sessionEvents.clear(); - } else { - var subList = sessionEvents.subList(0, eventsCount - 1); - result = List.copyOf(subList); - subList.clear(); - } - return result; - } - } - public List readLog(Integer maxEntries, Boolean clearLog) { synchronized (sessionMessages) { List messages = new ArrayList<>(); @@ -1033,11 +963,12 @@ public class WebSession extends AbstractSessionPersistent this.userContext.reset(); } - public synchronized void updateSMAuthInfo(SMAuthInfo smAuthInfo) throws DBException { - boolean contextChanged = userContext.refresh(smAuthInfo); + public synchronized boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException { + boolean contextChanged = super.updateSMSession(smAuthInfo); if (contextChanged) { refreshUserData(); } + return contextChanged; } @Override diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java index 30ac07e8d2..fa2c658c76 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java @@ -92,7 +92,7 @@ public class WebSessionAuthProcessor { webSession.removeAuthInfo(authConfiguration.getAuthProviderId()); } } - webSession.updateSMAuthInfo(authInfo); + webSession.updateSMSession(authInfo); WebUser curUser = webSession.getUser(); if (curUser == null) { //should never happen in theory since we should get the error much earlier diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index c6105717e9..5740669c8f 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -29,13 +29,19 @@ import org.jkiss.dbeaver.model.auth.SMAuthStatus; import org.jkiss.dbeaver.model.auth.SMCredentials; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.secret.DBSSecretController; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; import org.jkiss.utils.CommonUtils; +import java.util.Arrays; +import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; /** * Web user context. @@ -55,10 +61,13 @@ public class WebUserContext implements SMCredentialsProvider { private SMController securityController; private SMAdminController adminSecurityController; private DBSSecretController secretController; + private RMController rmController; + private Set accessibleProjectIds = new HashSet<>(); public WebUserContext(WebApplication application) throws DBException { this.application = application; this.securityController = application.createSecurityController(this); + this.rmController = application.createResourceController(this); setUserPermissions(getDefaultPermissions()); } @@ -72,28 +81,40 @@ public class WebUserContext implements SMCredentialsProvider { if (smAuthInfo.getAuthStatus() != SMAuthStatus.SUCCESS || smAuthInfo.getSmAuthToken() == null) { throw new DBCException("Authorization did not complete successfully"); } - var isNonAnonymousUserAuthorized = isAuthorizedInSecurityManager() && getUser() != null; - var authPermissions = smAuthInfo.getAuthPermissions(); - if (authPermissions == null) { + if (smAuthInfo.getAuthPermissions() == null) { throw new DBCException("Required information about session permissions is missing"); } - var isSessionChanged = !CommonUtils.equalObjects(smSessionId, authPermissions.getSessionId()); - if (isNonAnonymousUserAuthorized && isSessionChanged && !Objects.equals(getUserId(), authPermissions.getUserId())) { + return refresh(smAuthInfo.getSmAuthToken(), smAuthInfo.getSmRefreshToken(), smAuthInfo.getAuthPermissions()); + } + + public synchronized boolean refresh( + @NotNull String smAccessToken, + @Nullable String smRefreshToken, + @NotNull SMAuthPermissions smAuthPermissions + ) throws DBException { + var isNonAnonymousUserAuthorized = isAuthorizedInSecurityManager() && getUser() != null; + var isSessionChanged = !CommonUtils.equalObjects(smSessionId, smAuthPermissions.getSessionId()); + if (isNonAnonymousUserAuthorized && isSessionChanged && !Objects.equals(getUserId(), smAuthPermissions.getUserId())) { throw new DBCException("Another user is already logged in"); } this.smCredentials = new SMCredentials( - smAuthInfo.getSmAuthToken(), - authPermissions.getUserId(), - authPermissions.getPermissions() + smAccessToken, + smAuthPermissions.getUserId(), + smAuthPermissions.getPermissions() ); - this.refreshToken = smAuthInfo.getSmRefreshToken(); - setUserPermissions(authPermissions.getPermissions()); + this.refreshToken = smRefreshToken; + setUserPermissions(smAuthPermissions.getPermissions()); this.securityController = application.createSecurityController(this); this.adminSecurityController = application.getAdminSecurityController(this); this.secretController = application.getSecretController(this); + this.rmController = application.createResourceController(this); if (isSessionChanged) { - this.smSessionId = smAuthInfo.getAuthPermissions().getSessionId(); - setUser(authPermissions.getUserId() == null ? null : new WebUser(securityController.getCurrentUser())); + this.smSessionId = smAuthPermissions.getSessionId(); + setUser(smAuthPermissions.getUserId() == null ? null : new WebUser(securityController.getCurrentUser())); + this.accessibleProjectIds.clear(); + this.accessibleProjectIds.addAll( + Arrays.stream(rmController.listAccessibleProjects()).map(RMProject::getId).collect(Collectors.toSet()) + ); } return isSessionChanged; } @@ -196,4 +217,13 @@ public class WebUserContext implements SMCredentialsProvider { private Set getDefaultPermissions() { return application.getAppConfiguration().isAnonymousAccessEnabled() ? null : Set.of(); } + + public RMController getRmController() { + return rmController; + } + + @NotNull + public Set getAccessibleProjectIds() { + return accessibleProjectIds; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java index 65801e67cf..784c961079 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebEventUtils.java @@ -16,13 +16,15 @@ */ package io.cloudbeaver.utils; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.rm.RMResource; +import org.jkiss.dbeaver.model.websocket.WSConstants; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; +import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceUpdatedEvent; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; public class WebEventUtils { /** @@ -31,89 +33,124 @@ public class WebEventUtils { * @param project project of updated database * @param sessionId session id * @param datasourceId id of datasource - * @param eventType type of event + * @param eventAction type of event */ public static void addDataSourceUpdatedEvent( DBPProject project, String sessionId, String datasourceId, - CBEventConstants.EventType eventType) { + WSConstants.EventAction eventAction + ) { if (project == null) { return; } - WebAppUtils.getWebApplication().getEventController().addEvent( - new CBEvent( - CBEventConstants.CLOUDBEAVER_DATASOURCE_UPDATED, - sessionId, - Map.of("projectId", project.getId(), - "dataSourceIds", List.of(datasourceId), - "eventType", eventType) - ) - ); + WSEvent event = null; + switch (eventAction) { + case CREATE: + event = WSDataSourceEvent.create( + sessionId, + project.getId(), + List.of(datasourceId) + ); + break; + case DELETE: + event = WSDataSourceEvent.delete( + sessionId, + project.getId(), + List.of(datasourceId) + ); + break; + case UPDATE: + event = WSDataSourceEvent.update( + sessionId, + project.getId(), + List.of(datasourceId) + ); + break; + } + if (event == null) { + return; + } + WebAppUtils.getWebApplication().getEventController().addEvent(event); } public static void addNavigatorNodeUpdatedEvent( DBPProject project, String sessionId, String nodePath, - CBEventConstants.EventType eventType) { + WSConstants.EventAction eventAction + ) { if (project == null) { return; } - WebAppUtils.getWebApplication().getEventController().addEvent( - new CBEvent( - CBEventConstants.CLOUDBEAVER_DATASOURCE_FOLDER_UPDATED, - sessionId, - Map.of("projectId", project.getId(), - "nodePaths", List.of(nodePath), - "eventType", eventType) - ) - ); + WSEvent event = null; + switch (eventAction) { + case CREATE: + event = WSDatasourceFolderEvent.create( + sessionId, + project.getId(), + List.of(nodePath) + ); + break; + case DELETE: + event = WSDatasourceFolderEvent.delete( + sessionId, + project.getId(), + List.of(nodePath) + ); + break; + case UPDATE: + event = WSDatasourceFolderEvent.update( + sessionId, + project.getId(), + List.of(nodePath) + ); + break; + } + if (event == null) { + return; + } + WebAppUtils.getWebApplication().getEventController().addEvent(event); } public static void addRmResourceUpdatedEvent( String projectId, String sessionId, String resourcePath, - Object resourceParsedPath, - CBEventConstants.EventType eventType + RMResource[] resourceParsedPath, + WSConstants.EventAction eventAction ) { - addRmResourceUpdatedEvent( - sessionId, - generateRmResourceEventData( - projectId, - resourcePath, - resourceParsedPath, - eventType - ) - ); + WSEvent event = null; + switch (eventAction) { + case CREATE: + event = WSResourceUpdatedEvent.create( + sessionId, + projectId, + resourcePath, + resourceParsedPath + ); + break; + case DELETE: + event = WSResourceUpdatedEvent.delete( + sessionId, + projectId, + resourcePath, + resourceParsedPath + ); + break; + case UPDATE: + event = WSResourceUpdatedEvent.update( + sessionId, + projectId, + resourcePath, + resourceParsedPath + ); + break; + } + if (event == null) { + return; + } + WebAppUtils.getWebApplication().getEventController().addEvent(event); } - public static void addRmResourceUpdatedEvent(String sessionId, Map eventData) { - WebAppUtils.getWebApplication().getEventController().addEvent( - new CBEvent( - CBEventConstants.CLOUDBEAVER_RM_RESOURCE_UPDATED, - sessionId, - eventData - ) - ); - } - - public static Map generateRmResourceEventData( - String projectId, - String resourcePath, - Object resourceParsedPath, - CBEventConstants.EventType eventType - ) { - return new LinkedHashMap<>( - Map.of( - "projectId", projectId, - "resourcePath", resourcePath, - "resourceParsedPath", resourceParsedPath, - "eventType", eventType - ) - ); - } - - } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java similarity index 67% rename from server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java index 574c0082fa..bfcf81e425 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java @@ -14,22 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.events; +package io.cloudbeaver.websocket; -import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; -/** - * CB web event handlers - */ -public interface CBEventHandler { - /** - * Expected cb event type - */ - @NotNull - String getSupportedEventType(); +public interface CBWebSessionEventHandler { + void handeWebSessionEvent(WSEvent event) throws DBException; - /** - * Handle associated cb event - */ - void handleEvent(@NotNull CBEvent event); + void close(); } diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index 068f4a4c61..fd2ae91e39 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -22,6 +22,7 @@ Require-Bundle: org.eclipse.core.runtime;visibility:=reexport, com.google.gson;visibility:=reexport, org.jkiss.bundle.graphql.java;visibility:=reexport, org.jkiss.bundle.apache.dbcp, + org.jkiss.bundle.jetty.websocket, org.jkiss.dbeaver.net.ssh, io.cloudbeaver.model;visibility:=reexport, io.cloudbeaver.service.security;visibility:=reexport diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 7274609f60..17a19e2c37 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -46,11 +46,11 @@ - - - - - + + + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 0a0a730a00..74f5ad68a2 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -397,29 +397,6 @@ type LogEntry { stackTrace: String } -enum CBEventType { - cb_config_changed, - cb_datasource_updated, - cb_datasource_folder_updated, - cb_rm_resource_updated -} - -enum CBEventResourceType { - RM_RESOURCE, - DATASOURCE -} - -enum CBEventStatus { - TYPE_CREATE, - TYPE_DELETE, - TYPE_UPDATE -} - -type CBEvent { - eventType: CBEventType! - eventData: Object! -} - #################################################### # Input types #################################################### @@ -538,8 +515,6 @@ extend type Query { listProjects: [ ProjectInfo! ]! readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ]! - - readSessionEvents(maxEntries: Int!): [ CBEvent! ]! } extend type Mutation { diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls new file mode 100644 index 0000000000..aeb2fc6b6d --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -0,0 +1,100 @@ +#################################################### +# Events API +#################################################### + +# Events sent by server +enum CBServerEventId { + cb_config_changed, + + cb_datasource_created, + cb_datasource_updated, + cb_datasource_deleted, + + cb_datasource_folder_created, + cb_datasource_folder_updated, + cb_datasource_folder_deleted, + + cb_rm_resource_created + cb_rm_resource_updated + cb_rm_resource_deleted +} + +# Events sent by client +enum CBClientEventId { + cb_client_topic_subscribe, + cb_client_topic_unsubscribe, + cb_client_projects_active +} + +# Client subscribes on topic to receive only related events +enum CBEventTopic { + cb_config, + cb_datasource, + cb_datasource_folder, + cb_scripts, + cb_projects +} + +# Base server event interface +interface CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic +} + +# Base client event interface +interface CBClientEvent { + id: CBClientEventId! + topicId: CBEventTopic +} + +# Datasource folder event +type CBDatasourceFolderEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + nodePaths: [String!]! # list of folders node ids + projectId: String! +} + +# Datasource event +type CBDatasourceEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + dataSourceIds: [String!]! # list of datasources ids + projectId: String! +} + +# Resource manager event +type CBRMEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + resourcePath: String! + projectId: String! +} + +# Server Config event +type CBConfigEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic +} + +# Topic subscription event +type CBTopicEvent implements CBClientEvent { + id: CBClientEventId! + topicId: CBEventTopic! # topic to subscribe / unsubscribe +} + +# Active projects event +type CBProjectsActiveEvent implements CBClientEvent { + id: CBClientEventId! + topicId: CBEventTopic + projects: [String!]! # list of active projects +} + +extend type Query { + emptyEvent: Boolean +} + +extend type Mutation { + emptyEventMutation: Boolean +} + diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index b6da5f5a42..21e9ec065a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -21,9 +21,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.NoAuthCredentialsProvider; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; -import io.cloudbeaver.events.CBEventController; import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; @@ -54,6 +51,8 @@ import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.security.*; +import org.jkiss.dbeaver.model.websocket.event.WSEventController; +import org.jkiss.dbeaver.model.websocket.event.WSServerConfigurationChangedEvent; import org.jkiss.dbeaver.registry.BaseApplicationImpl; import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; import org.jkiss.dbeaver.runtime.DBWorkbench; @@ -131,7 +130,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat private String localHostAddress; private final List localInetAddresses = new ArrayList<>(); - protected final CBEventController eventController = new CBEventController(); + protected final WSEventController eventController = new WSEventController(); private WebSessionManager sessionManager; @@ -837,7 +836,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat if (credentialsProvider instanceof WebSession) { sessionId = ((WebSession) credentialsProvider).getSessionId(); } - eventController.addEvent(new CBEvent(CBEventConstants.CLOUDBEAVER_CONFIG_CHANGED, sessionId)); + eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId)); } protected Map readRuntimeConfigurationProperties() throws DBException { @@ -1109,7 +1108,7 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat } @Override - public CBEventController getEventController() { + public WSEventController getEventController() { return eventController; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java deleted file mode 100644 index 825b71203f..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 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.events; - -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; -import io.cloudbeaver.model.session.WebSession; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.data.json.JSONUtils; - -import java.util.List; - -/** - * Notify all active user session that datasource has been updated - */ -public class CBDataSourceUpdatedEventHandlerImpl extends CBProjectUpdatedEventHandler { - @NotNull - @Override - public String getSupportedEventType() { - return CBEventConstants.CLOUDBEAVER_DATASOURCE_UPDATED; - } - - @Override - protected void updateSessionData(WebSession activeUserSession, CBEvent event) { - String projectId = JSONUtils.getString(event.getEventData(), "projectId"); - List dataSourceIds = JSONUtils.getStringList(event.getEventData(), "dataSourceIds"); - CBEventConstants.EventType eventType = - CBEventConstants.EventType.valueOf(JSONUtils.getString(event.getEventData(), "eventType")); - WebProjectImpl project = activeUserSession.getProjectById(projectId); - if (project == null) { - return; - } - activeUserSession.updateProjectConnection(project, dataSourceIds, eventType); - activeUserSession.addSessionEvent(event); - } -} - diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBFolderUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBFolderUpdatedEventHandlerImpl.java deleted file mode 100644 index c9418cb981..0000000000 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBFolderUpdatedEventHandlerImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * DBeaver - Universal Database Manager - * Copyright (C) 2010-2022 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.events; - -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; -import io.cloudbeaver.model.session.WebSession; -import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.model.data.json.JSONUtils; - -import java.util.List; - -/** - * Notify all active user session that datasource has been updated - */ -public class CBFolderUpdatedEventHandlerImpl extends CBProjectUpdatedEventHandler { - @NotNull - @Override - public String getSupportedEventType() { - return CBEventConstants.CLOUDBEAVER_DATASOURCE_FOLDER_UPDATED; - } - - @Override - protected void updateSessionData(WebSession activeUserSession, CBEvent event) { - String projectId = JSONUtils.getString(event.getEventData(), "projectId"); - List nodePaths = JSONUtils.getStringList(event.getEventData(), "nodePaths"); - WebProjectImpl project = activeUserSession.getProjectById(projectId); - if (project == null || nodePaths.isEmpty()) { - return; - } - - project.getDataSourceRegistry().refreshConfig(); - activeUserSession.getNavigatorModel().getRoot().getProjectNode(project).getDatabases().refreshChildren(); - activeUserSession.addSessionEvent(event); - } -} - diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSConfigurationChangedEventHandler.java similarity index 62% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSConfigurationChangedEventHandler.java index 5d7bbe7b14..dbe4002c0a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBConfigurationChangedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSConfigurationChangedEventHandler.java @@ -16,14 +16,13 @@ */ package io.cloudbeaver.server.events; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; -import io.cloudbeaver.events.CBEventHandler; -import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.server.CBPlatform; -import io.cloudbeaver.service.session.WebSessionManager; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; import org.jkiss.utils.CommonUtils; import java.util.Collection; @@ -31,21 +30,21 @@ import java.util.Collection; /** * Notify all active user session that configuration has been changed */ -public class CBConfigurationChangedEventHandler implements CBEventHandler { - private static final Log log = Log.getLog(CBConfigurationChangedEventHandler.class); +public class WSConfigurationChangedEventHandler implements WSEventHandler { + private static final Log log = Log.getLog(WSConfigurationChangedEventHandler.class); @NotNull @Override - public String getSupportedEventType() { - return CBEventConstants.CLOUDBEAVER_CONFIG_CHANGED; + public String getSupportedTopicId() { + return WSEventTopic.SERVER_CONFIG.getTopicId(); } @Override - public void handleEvent(@NotNull CBEvent event) { - log.debug(getSupportedEventType() + " event handled"); - Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + public void handleEvent(@NotNull WSEvent event) { + log.debug(getSupportedTopicId() + " event handled"); + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); - for (WebSession activeUserSession : allSessions) { + for (var activeUserSession : allSessions) { if (CommonUtils.equalObjects(activeUserSession.getSessionId(), event.getSessionId())) { continue; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java new file mode 100644 index 0000000000..895ad9ed70 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDataSourceUpdatedEventHandlerImpl.java @@ -0,0 +1,63 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.events; + +import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent; + +/** + * Notify all active user session that datasource has been updated + */ +public class WSDataSourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler { + @NotNull + @Override + public String getSupportedTopicId() { + return WSEventTopic.DATASOURCE.getTopicId(); + } + + @Override + protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { + if (!(event instanceof WSDataSourceEvent)) { + return; + } + var dsUpdateEvent = (WSDataSourceEvent) event; + if (!activeUserSession.isProjectAccessible(dsUpdateEvent.getProjectId())) { + return; + } + var eventType = WSEventType.valueById(event.getId()); + if (eventType == null) { + return; + } + if (activeUserSession instanceof WebSession) { + var webSession = (WebSession) activeUserSession; + WebProjectImpl project = webSession.getProjectById(dsUpdateEvent.getProjectId()); + webSession.updateProjectConnection( + project, + dsUpdateEvent.getDataSourceIds(), + eventType + ); + } + activeUserSession.addSessionEvent(event); + } +} + diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java new file mode 100644 index 0000000000..106ca3eb96 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSFolderUpdatedEventHandlerImpl.java @@ -0,0 +1,57 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.events; + +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; +import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent; +import org.jkiss.utils.CommonUtils; + +/** + * Notify all active user session that datasource has been updated + */ +public class WSFolderUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler { + @NotNull + @Override + public String getSupportedTopicId() { + return WSEventTopic.DATASOURCE_FOLDER.getTopicId(); + } + + @Override + protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { + if (!(event instanceof WSDatasourceFolderEvent)) { + return; + } + var dsFolderUpdateEvent = (WSDatasourceFolderEvent) event; + boolean isProjectAccessible = activeUserSession.isProjectAccessible(dsFolderUpdateEvent.getProjectId()); + if (!isProjectAccessible || CommonUtils.isEmpty(dsFolderUpdateEvent.getNodePaths())) { + return; + } + + if (activeUserSession instanceof WebSession) { + var webSession = (WebSession) activeUserSession; + var project = webSession.getProjectById(dsFolderUpdateEvent.getProjectId()); + project.getDataSourceRegistry().refreshConfig(); + webSession.getNavigatorModel().getRoot().getProjectNode(project).getDatabases().refreshChildren(); + } + activeUserSession.addSessionEvent(event); + } +} + diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBProjectUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java similarity index 62% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBProjectUpdatedEventHandler.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java index fe5158fafa..04a1cf72d2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBProjectUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSProjectUpdatedEventHandler.java @@ -16,12 +16,12 @@ */ package io.cloudbeaver.server.events; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventHandler; -import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.server.CBPlatform; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.websocket.WSEventHandler; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; import org.jkiss.utils.CommonUtils; import java.util.Collection; @@ -29,18 +29,18 @@ import java.util.Collection; /** * Notify all active user session that project has been updated */ -public abstract class CBProjectUpdatedEventHandler implements CBEventHandler { - private static final Log log = Log.getLog(CBProjectUpdatedEventHandler.class); +public abstract class WSProjectUpdatedEventHandler implements WSEventHandler { + private static final Log log = Log.getLog(WSProjectUpdatedEventHandler.class); @NotNull @Override - public abstract String getSupportedEventType(); + public abstract String getSupportedTopicId(); @Override - public void handleEvent(@NotNull CBEvent event) { - log.debug(getSupportedEventType() + " event handled"); - Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); - for (WebSession activeUserSession : allSessions) { + public void handleEvent(@NotNull WSEvent event) { + log.debug(getSupportedTopicId() + " event handled"); + Collection allSessions = CBPlatform.getInstance().getSessionManager().getAllActiveSessions(); + for (var activeUserSession : allSessions) { if (CommonUtils.equalObjects(activeUserSession.getSessionId(), event.getSessionId())) { continue; } @@ -48,5 +48,5 @@ public abstract class CBProjectUpdatedEventHandler implements CBEventHandler { } } - protected abstract void updateSessionData(WebSession activeUserSession, CBEvent event); + protected abstract void updateSessionData(BaseWebSession activeUserSession, WSEvent event); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBRmResourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java similarity index 56% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBRmResourceUpdatedEventHandlerImpl.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java index 7f0b1f05a4..c52edb9cad 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBRmResourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSRmResourceUpdatedEventHandlerImpl.java @@ -19,70 +19,75 @@ package io.cloudbeaver.server.events; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; +import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.rm.RMEvent; import org.jkiss.dbeaver.model.rm.RMEventManager; import org.jkiss.dbeaver.model.rm.RMResource; -import org.jkiss.utils.CommonUtils; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEventTopic; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.resource.WSResourceUpdatedEvent; import java.util.Arrays; import java.util.List; -import java.util.Map; /** * Notify all active user session that rm resource has been updated */ -public class CBRmResourceUpdatedEventHandlerImpl extends CBProjectUpdatedEventHandler { +public class WSRmResourceUpdatedEventHandlerImpl extends WSProjectUpdatedEventHandler { private static final Gson gson = new GsonBuilder().create(); - private static final String RESOURCE_PARSED_PATH = "resourceParsedPath"; - public static final String RESOURCE_PATH = "resourcePath"; - public static final String EVENT_TYPE = "eventType"; - public static final String PROJECT_ID = "projectId"; @NotNull @Override - public String getSupportedEventType() { - return CBEventConstants.CLOUDBEAVER_RM_RESOURCE_UPDATED; + public String getSupportedTopicId() { + return WSEventTopic.RM_SCRIPTS.getTopicId(); } @Override - protected void updateSessionData(WebSession activeUserSession, CBEvent event) { - String projectId = JSONUtils.getString(event.getEventData(), PROJECT_ID); - WebProjectImpl project = activeUserSession.getProjectById(projectId); - if (project == null) { + protected void updateSessionData(BaseWebSession activeUserSession, WSEvent event) { + if (!(event instanceof WSResourceUpdatedEvent)) { return; } - String eventType = JSONUtils.getString(event.getEventData(), EVENT_TYPE); - String resourcePath = JSONUtils.getString(event.getEventData(), RESOURCE_PATH); - if (eventType == null || resourcePath == null) { + var resourceUpdateEvent = (WSResourceUpdatedEvent) event; + String projectId = resourceUpdateEvent.getProjectId(); + if (!activeUserSession.isProjectAccessible(projectId)) { return; } - Object parsedResourcePath = event.getEventData().get(RESOURCE_PARSED_PATH); + if (resourceUpdateEvent.getResourcePath() == null) { + return; + } + Object parsedResourcePath = resourceUpdateEvent.getResourceParsedPath(); RMResource[] resourceParsedPath; if (parsedResourcePath instanceof RMResource[]) { resourceParsedPath = (RMResource[]) parsedResourcePath; } else { resourceParsedPath = gson.fromJson(gson.toJson(parsedResourcePath), RMResource[].class); } - acceptChangesInNavigatorTree(eventType, resourceParsedPath, project); + var eventType = WSEventType.valueById(resourceUpdateEvent.getId()); + if (eventType == null) { + return; + } + if (activeUserSession instanceof WebSession) { + var webSession = (WebSession) activeUserSession; + acceptChangesInNavigatorTree(eventType, resourceParsedPath, webSession.getProjectById(projectId)); + } activeUserSession.addSessionEvent(event); } - private void acceptChangesInNavigatorTree(String eventType, RMResource[] resourceParsedPath, WebProjectImpl project) { + private void acceptChangesInNavigatorTree(WSEventType eventType, + RMResource[] resourceParsedPath, + WebProjectImpl project) { List rmResourcePath = Arrays.asList(resourceParsedPath); - if (eventType.equals("TYPE_CREATE")) { + if (eventType == WSEventType.RM_RESOURCE_CREATED) { RMEventManager.fireEvent( new RMEvent(RMEvent.Action.RESOURCE_ADD, project.getRmProject(), rmResourcePath) ); - } else if (eventType.equals("TYPE_DELETE")) { + } else if (eventType == WSEventType.RM_RESOURCE_DELETED) { RMEventManager.fireEvent( new RMEvent(RMEvent.Action.RESOURCE_DELETE, project.getRmProject(), 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 b9c5535502..2df304360a 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 @@ -1,3 +1,19 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.jetty; import io.cloudbeaver.registry.WebServiceRegistry; @@ -6,6 +22,7 @@ import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; import io.cloudbeaver.server.servlets.CBStatusServlet; +import io.cloudbeaver.server.websockets.CBJettyWebSocketManager; import io.cloudbeaver.service.DBWServiceBindingServlet; import org.eclipse.jetty.server.*; import org.eclipse.jetty.server.session.DefaultSessionCache; @@ -16,6 +33,7 @@ import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.runtime.DBWorkbench; @@ -26,6 +44,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Arrays; public class CBJettyServer { @@ -75,7 +94,7 @@ public class CBJettyServer { servletContextHandler.addEventListener(new CBServerContextListener()); // Add extensions from services - + CBJettyServletContext servletContext = new CBJettyServletContext(servletContextHandler); for (DBWServiceBindingServlet wsd : WebServiceRegistry.getInstance().getWebServices(DBWServiceBindingServlet.class)) { try { @@ -89,6 +108,18 @@ public class CBJettyServer { server.setHandler(servletContextHandler); + var serverConnector = new ServerConnector(server); + server.addConnector(serverConnector); + JettyWebSocketServletContainerInitializer.configure(servletContextHandler, + (context, wsContainer) -> { + wsContainer.setIdleTimeout(Duration.ofMinutes(5)); + // Add websockets + wsContainer.addMapping( + application.getServicesURI() + "ws/*", + new CBJettyWebSocketManager(application.getSessionManager()) + ); + } + ); ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler(); //errorHandler.addErrorPage(404, "/missing.html"); servletContextHandler.setErrorHandler(errorHandler); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java new file mode 100644 index 0000000000..5cf2b1dabe --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -0,0 +1,117 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.websockets; + +import com.google.gson.Gson; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.websocket.CBWebSessionEventHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.websocket.WSConstants; +import org.jkiss.dbeaver.model.websocket.event.WSClientEvent; +import org.jkiss.dbeaver.model.websocket.event.WSEvent; + +import java.io.IOException; + +public class CBEventsWebSocket extends WebSocketAdapter implements CBWebSessionEventHandler { + private static final Gson gson = new Gson(); + private static final Log log = Log.getLog(CBEventsWebSocket.class); + + @NotNull + private final BaseWebSession webSession; + + public CBEventsWebSocket(@NotNull BaseWebSession webSession) { + this.webSession = webSession; + } + + @Override + public void onWebSocketConnect(Session session) { + super.onWebSocketConnect(session); + this.webSession.addEventHandler(this); + log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); + } + + @Override + public void onWebSocketText(String message) { + super.onWebSocketText(message); + var clientEvent = gson.fromJson(message, WSClientEvent.class); + if (clientEvent.getId() == null) { + webSession.addSessionError( + new DBWebException("Invalid websocket event: " + message) + ); + } + switch (clientEvent.getId()) { + case WSConstants.ClientEvents.TOPIC_SUBSCRIBE: { + this.webSession.subscribeOnEventTopic(clientEvent.getTopicId()); + break; + } + case WSConstants.ClientEvents.TOPIC_UNSUBSCRIBE: { + this.webSession.unsubscribeFromEventTopic(clientEvent.getTopicId()); + break; + } + default: + var e = new DBWebException("Unknown websocket client event: " + clientEvent.getId()); + log.error(e.getMessage(), e); + webSession.addSessionError(e); + } + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + super.onWebSocketClose(statusCode, reason); + this.webSession.removeEventHandler(this); + log.debug("Socket Closed: [" + statusCode + "] " + reason); + } + + @Override + public void onWebSocketError(Throwable cause) { + super.onWebSocketError(cause); + log.error(cause.getMessage(), cause); + webSession.addSessionError(cause); + } + + public void awaitClosure() throws InterruptedException { + log.debug("Awaiting closure from remote"); + } + + @Override + public void handeWebSessionEvent(WSEvent event) { + if (isNotConnected()) { + return; + } + try { + getRemote().sendString(gson.toJson(event)); + } catch (IOException e) { + log.error("Failed to send websocket message", e); + webSession.addSessionError(e); + } + } + + @Override + public void close() { + getSession().close(); + onWebSocketClose(WSConstants.NORMAL_STATUS, "Closed by web session"); + } + + @NotNull + public BaseWebSession getWebSession() { + return webSession; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java new file mode 100644 index 0000000000..e4a10e8bd5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -0,0 +1,101 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.websockets; + +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.session.WebSessionManager; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class CBJettyWebSocketManager implements JettyWebSocketCreator { + private static final Log log = Log.getLog(CBJettyWebSocketManager.class); + private final Map socketBySessionId = new ConcurrentHashMap<>(); + private final WebSessionManager webSessionManager; + + public CBJettyWebSocketManager(WebSessionManager webSessionManager) { + this.webSessionManager = webSessionManager; + new WebSocketPingPongJob(CBPlatform.getInstance(), this).scheduleMonitor(); + } + + @Override + public Object createWebSocket(JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) { + BaseWebSession webSession; + try { + webSession = resolveWebSession(request); + } catch (DBException e) { + log.error("Error resolve websocket session", e); + return null; + } + if (webSession == null) { + log.error("CloudBeaver session not found"); + return null; + } + var webSessionId = webSession.getSessionId(); + var oldWebSocket = socketBySessionId.get(webSessionId); + if (oldWebSocket != null) { + oldWebSocket.close(); + } + var newWebSocket = new CBEventsWebSocket(webSession); + socketBySessionId.put(webSessionId, newWebSocket); + return newWebSocket; + } + + @Nullable + private BaseWebSession resolveWebSession(JettyServerUpgradeRequest request) throws DBException { + if (request.getHttpServletRequest().getSession() == null) { + return webSessionManager.getHeadlessSession(request.getHttpServletRequest(), true); + } + var webSessionId = request.getHttpServletRequest().getSession().getId(); + var webSession = webSessionManager.getSession(webSessionId); + return webSession != null + ? webSession + : webSessionManager.getHeadlessSession(request.getHttpServletRequest(), true); + } + + public void sendPing() { + //remove expired sessions + socketBySessionId.entrySet() + .removeIf(entry -> + entry.getValue().isNotConnected() || webSessionManager.getSession(entry.getKey()) == null + ); + + socketBySessionId.entrySet() + .stream() + .parallel() + .forEach( + entry -> { + var sessionId = entry.getKey(); + var webSocket = entry.getValue(); + try { + webSocket.getRemote().sendPing(ByteBuffer.wrap("cb-ping".getBytes(StandardCharsets.UTF_8))); + } catch (Exception e) { + log.error("Failed to send ping in web socket: " + sessionId); + } + } + ); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java new file mode 100644 index 0000000000..a5017ff667 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongJob.java @@ -0,0 +1,59 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2022 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.websockets; + +import io.cloudbeaver.server.CBPlatform; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +/** + * WebSessionMonitorJob + */ +class WebSocketPingPongJob extends AbstractJob { + private static final int INTERVAL = 1000 * 60 * 2; // once per 2 min + private final CBPlatform platform; + private final CBJettyWebSocketManager webSocketManager; + + public WebSocketPingPongJob(CBPlatform platform, CBJettyWebSocketManager webSocketManager) { + super("WebSocket monitor"); + this.platform = platform; + setUser(false); + setSystem(true); + this.webSocketManager = webSocketManager; + } + + @Override + protected IStatus run(DBRProgressMonitor monitor) { + if (platform.isShuttingDown()) { + return Status.OK_STATUS; + } + + webSocketManager.sendPing(); + + if (!platform.isShuttingDown()) { + scheduleMonitor(); + } + return Status.OK_STATUS; + } + + void scheduleMonitor() { + schedule(INTERVAL); + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index 53e4022dc5..f3bfb12c11 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -20,7 +20,6 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.WebObjectId; import io.cloudbeaver.WebProjectAction; -import io.cloudbeaver.events.CBEvent; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; @@ -85,9 +84,6 @@ public interface DBWServiceCore extends DBWService { @WebAction List readSessionLog(@NotNull WebSession webSession, Integer maxEntries, Boolean clearEntries) throws DBWebException; - @WebAction(requirePermissions = {}) - List readSessionEvents(@Nullable WebSession webSession, Integer maxEntries) throws DBWebException; - @WebAction(requirePermissions = {}) boolean closeSession(HttpServletRequest request) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 5e079c041d..1e8cd06c3e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -73,9 +73,6 @@ public class WebServiceBindingCore extends WebServiceBindingBase getWebSession(env), getProjectReference(env), env.getArgument("id"))) .dataFetcher("listProjects", env -> getService(env).getProjects(getWebSession(env))) - .dataFetcher("readSessionEvents", env -> getService(env).readSessionEvents( - findWebSession(env), env.getArgument("maxEntries") - )) .dataFetcher("readSessionLog", env -> { // CB-90. Log read mustn't extend session lifetime and mustn't fail if there is no session. WebSession session = findWebSession(env); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 89b6bdf323..ef52077c8a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -21,8 +21,6 @@ import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.events.CBEvent; -import io.cloudbeaver.events.CBEventConstants; import io.cloudbeaver.model.*; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebHandlerRegistry; @@ -52,6 +50,7 @@ import org.jkiss.dbeaver.model.net.DBWTunnel; import org.jkiss.dbeaver.model.net.ssh.SSHImplementation; import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; @@ -62,10 +61,7 @@ import org.jkiss.utils.CommonUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -244,25 +240,18 @@ public class WebServiceCore implements DBWServiceCore { return webSession.readLog(maxEntries, clearEntries); } - @Override - public List readSessionEvents(@Nullable WebSession webSession, Integer maxEntries) throws DBWebException { - if (webSession == null) { - return Collections.emptyList(); - } - return webSession.getSessionEvents(maxEntries); - } - @Override public boolean closeSession(HttpServletRequest request) throws DBWebException { try { - WebSession webSession = CBPlatform.getInstance().getSessionManager().closeSession(request); - if (webSession != null) { + var baseWebSession = CBPlatform.getInstance().getSessionManager().closeSession(request); + if (baseWebSession instanceof WebSession) { + var webSession = (WebSession) baseWebSession; for (WebSessionHandlerDescriptor hd : WebHandlerRegistry.getInstance().getSessionHandlers()) { try { hd.getInstance().handleSessionClose(webSession); } catch (Exception e) { log.error("Error calling session handler '" + hd.getId() + "'", e); - webSession.addSessionError(e); + baseWebSession.addSessionError(e); } } return true; @@ -412,7 +401,7 @@ public class WebServiceCore implements DBWServiceCore { webSession.getProjectById(projectId), webSession.getSessionId(), connectionInfo.getId(), - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); return connectionInfo; } @@ -437,6 +426,7 @@ public class WebServiceCore implements DBWServiceCore { if (!CommonUtils.isEmpty(config.getName())) { dataSource.setName(config.getName()); } + String oldDescription = dataSource.getDescription(); if (config.getDescription() != null) { dataSource.setDescription(config.getDescription()); } @@ -444,8 +434,8 @@ public class WebServiceCore implements DBWServiceCore { dataSource.setFolder(config.getFolder() != null ? sessionRegistry.getFolder(config.getFolder()) : null); WebServiceUtils.setConnectionConfiguration(dataSource.getDriver(), dataSource.getConnectionConfiguration(), config); - - boolean sendEvent = sendUpdateConnectionEvent(config, dataSource, oldConnectionConfig); + + boolean sendEvent = sendUpdateConnectionEvent(config, dataSource, oldConnectionConfig, oldDescription); WebServiceUtils.saveAuthProperties( dataSource, @@ -466,7 +456,7 @@ public class WebServiceCore implements DBWServiceCore { webSession.getProjectById(projectId), webSession.getSessionId(), connectionInfo.getId(), - CBEventConstants.EventType.TYPE_UPDATE + WSConstants.EventAction.UPDATE ); } return connectionInfo; @@ -478,11 +468,15 @@ public class WebServiceCore implements DBWServiceCore { private boolean sendUpdateConnectionEvent( @NotNull WebConnectionConfig config, @NotNull DBPDataSourceContainer dataSource, - @NotNull DBPConnectionConfiguration oldConnectionConfig + @NotNull DBPConnectionConfiguration oldConnectionConfig, + @Nullable String oldDescription ) { if (!oldConnectionConfig.equals(dataSource.getConnectionConfiguration())) { return true; } + if (!Objects.equals(oldDescription, dataSource.getDescription())) { + return true; + } if (dataSource.getProject().isUseSecretStorage()) { return dataSource.isSharedCredentials() != config.isSharedCredentials(); } @@ -504,7 +498,7 @@ public class WebServiceCore implements DBWServiceCore { webSession.getProjectById(projectId), webSession.getSessionId(), connectionId, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); return true; } @@ -585,7 +579,7 @@ public class WebServiceCore implements DBWServiceCore { webSession.getProjectById(projectId), webSession.getSessionId(), connectionInfo.getId(), - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); return connectionInfo; } catch (DBException e) { @@ -788,7 +782,7 @@ public class WebServiceCore implements DBWServiceCore { session.getProjectById(projectId), session.getSessionId(), DBNLocalFolder.makeLocalFolderItemPath(newFolder), - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); return folderInfo; } catch (DBException e) { @@ -813,13 +807,13 @@ public class WebServiceCore implements DBWServiceCore { session.getProjectById(projectId), session.getSessionId(), oldFolderNode, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); WebEventUtils.addNavigatorNodeUpdatedEvent( session.getProjectById(projectId), session.getSessionId(), newFolderNode, - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); return folderInfo; } @@ -844,7 +838,7 @@ public class WebServiceCore implements DBWServiceCore { session.getProjectById(projectId), session.getSessionId(), folderNode, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); } catch (DBException e) { throw new DBWebException(e.getMessage(), e); @@ -864,7 +858,7 @@ public class WebServiceCore implements DBWServiceCore { webSession.getProjectById(projectId), webSession.getSessionId(), id, - CBEventConstants.EventType.TYPE_UPDATE); + WSConstants.EventAction.UPDATE); return connectionInfo; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 6907bfe665..125f549ff1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -19,7 +19,6 @@ package io.cloudbeaver.service.navigator.impl; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.events.CBEventConstants; import io.cloudbeaver.model.WebCommandContext; import io.cloudbeaver.model.WebConnectionInfo; import io.cloudbeaver.model.rm.DBNAbstractResourceManagerNode; @@ -53,6 +52,7 @@ import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.DBSObjectContainer; import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; +import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; @@ -393,13 +393,13 @@ public class WebServiceNavigator implements DBWServiceNavigator { node.getOwnerProject(), session.getSessionId(), oldNodePath, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); WebEventUtils.addNavigatorNodeUpdatedEvent( node.getOwnerProject(), session.getSessionId(), newNodePath, - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); } @@ -432,14 +432,14 @@ public class WebServiceNavigator implements DBWServiceNavigator { session.getSessionId(), oldResourcePath, oldRmResourcePath, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); WebEventUtils.addRmResourceUpdatedEvent( projectId, session.getSessionId(), newResourcePath, newRmResourcePath, - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); } @@ -494,7 +494,7 @@ public class WebServiceNavigator implements DBWServiceNavigator { session.getProjectById(projectId), session.getSessionId(), nodePath, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); } else if (node instanceof DBNResourceManagerResource) { DBNResourceManagerResource rmResource = ((DBNResourceManagerResource) node); @@ -507,7 +507,7 @@ public class WebServiceNavigator implements DBWServiceNavigator { session.getSessionId(), resourcePath, rmResourcePath, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); } } @@ -563,7 +563,7 @@ public class WebServiceNavigator implements DBWServiceNavigator { node.getOwnerProject(), session.getSessionId(), ((DBNDataSource) node).getDataSourceContainer().getId(), - CBEventConstants.EventType.TYPE_UPDATE + WSConstants.EventAction.UPDATE ); } else if (node instanceof DBNLocalFolder) { DBPDataSourceFolder parentFolder = WebConnectionFolderUtils.getParentFolder(folderNode); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/SMTokenCredentialProvider.java similarity index 53% rename from server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/SMTokenCredentialProvider.java index ebe71a0b4e..5e5048e0ae 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/events/CBEventConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/SMTokenCredentialProvider.java @@ -14,23 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cloudbeaver.events; +package io.cloudbeaver.service.session; -/** - * CB event constants - */ -public interface CBEventConstants { +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.auth.SMCredentials; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; - enum EventType { - TYPE_CREATE, - TYPE_DELETE, - TYPE_UPDATE +import java.util.Set; + +public class SMTokenCredentialProvider implements SMCredentialsProvider { + private final String smAccessToken; + + public SMTokenCredentialProvider(String smAccessToken) { + this.smAccessToken = smAccessToken; } - String CLOUDBEAVER_CONFIG_CHANGED = "cb_config_changed"; - String CLOUDBEAVER_DATASOURCE_UPDATED = "cb_datasource_updated"; - String CLOUDBEAVER_DATASOURCE_FOLDER_UPDATED = "cb_datasource_folder_updated"; - String CLOUDBEAVER_RM_RESOURCE_UPDATED = "cb_rm_resource_updated"; - - + @Nullable + @Override + public SMCredentials getActiveUserCredentials() { + return new SMCredentials(smAccessToken, null, Set.of()); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java index d7a76ab204..71b4dfd1b5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java @@ -17,6 +17,8 @@ package io.cloudbeaver.service.session; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebHeadlessSession; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionAuthProcessor; import io.cloudbeaver.registry.WebHandlerRegistry; @@ -29,6 +31,8 @@ import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; +import org.jkiss.dbeaver.model.security.user.SMAuthPermissions; +import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; @@ -46,16 +50,16 @@ public class WebSessionManager { private static final Log log = Log.getLog(WebSessionManager.class); private final CBApplication application; - private final Map sessionMap = new HashMap<>(); + private final Map sessionMap = new HashMap<>(); public WebSessionManager(CBApplication application) { this.application = application; } - public WebSession closeSession(@NotNull HttpServletRequest request) throws DBException { + public BaseWebSession closeSession(@NotNull HttpServletRequest request) throws DBException { HttpSession session = request.getSession(); if (session != null) { - WebSession webSession; + BaseWebSession webSession; synchronized (sessionMap) { webSession = sessionMap.remove(session.getId()); } @@ -72,7 +76,8 @@ public class WebSessionManager { return application; } - public boolean touchSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { + public boolean touchSession(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response) throws DBWebException { WebSession webSession = getWebSession(request, response, false); long maxSessionIdleTime = CBApplication.getInstance().getMaxSessionIdleTime(); webSession.updateInfo(request, response, maxSessionIdleTime); @@ -80,7 +85,8 @@ public class WebSessionManager { } @NotNull - public WebSession getWebSession(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { + public WebSession getWebSession(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response) throws DBWebException { return getWebSession(request, response, true); } @@ -95,8 +101,8 @@ public class WebSessionManager { String sessionId = httpSession.getId(); WebSession webSession; synchronized (sessionMap) { - webSession = sessionMap.get(sessionId); - if (webSession == null) { + var baseWebSession = sessionMap.get(sessionId); + if (baseWebSession == null) { try { webSession = createWebSessionImpl(httpSession); } catch (DBException e) { @@ -115,6 +121,10 @@ public class WebSessionManager { log.debug("> New web session '" + webSession.getSessionId() + "'"); } } else { + if (!(baseWebSession instanceof WebSession)) { + 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"))) { @@ -154,7 +164,7 @@ public class WebSessionManager { } @Nullable - public WebSession getWebSession(@NotNull String sessionId) { + public BaseWebSession getSession(@NotNull String sessionId) { synchronized (sessionMap) { return sessionMap.get(sessionId); } @@ -164,8 +174,11 @@ public class WebSessionManager { public WebSession findWebSession(HttpServletRequest request) { String sessionId = request.getSession().getId(); synchronized (sessionMap) { - WebSession webSession = sessionMap.get(sessionId); - return webSession; + var session = sessionMap.get(sessionId); + if (session instanceof WebSession) { + return (WebSession) session; + } + return null; } } @@ -187,10 +200,10 @@ public class WebSessionManager { maxSessionIdleTime = 60 * 60 * 1000 * 24 * 7; } - List expiredList = new ArrayList<>(); + List expiredList = new ArrayList<>(); synchronized (sessionMap) { - for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { - WebSession session = iterator.next(); + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + var session = iterator.next(); long idleMillis = System.currentTimeMillis() - session.getLastAccessTimeMillis(); if (idleMillis >= maxSessionIdleTime) { iterator.remove(); @@ -199,16 +212,54 @@ public class WebSessionManager { } } - for (WebSession session : expiredList) { + for (var session : expiredList) { log.debug("> Expire session '" + session.getSessionId() + "'"); session.close(); } } - public Collection getAllActiveSessions() { + public Collection getAllActiveSessions() { synchronized (sessionMap) { return sessionMap.values(); } } + @Nullable + public WebHeadlessSession getHeadlessSession(HttpServletRequest request, boolean create) throws DBException { + String smAccessToken = request.getHeader(WSConstants.WS_AUTH_HEADER); + if (CommonUtils.isEmpty(smAccessToken)) { + return null; + } + synchronized (sessionMap) { + var httpSession = request.getSession(); + var tempCredProvider = new SMTokenCredentialProvider(smAccessToken); + SMAuthPermissions authPermissions = application.createSecurityController(tempCredProvider).getTokenPermissions(); + var sessionId = httpSession != null ? httpSession.getId() + : authPermissions.getSessionId(); + + var existSession = sessionMap.get(sessionId); + + if (existSession instanceof WebHeadlessSession) { + return (WebHeadlessSession) existSession; + } + if (existSession != null) { + //session exist but it not headless session + return null; + } + if (!create) { + return null; + } + var headlessSession = new WebHeadlessSession( + sessionId, + application + ); + headlessSession.getUserContext().refresh( + smAccessToken, + null, + authPermissions + ); + sessionMap.put(sessionId, headlessSession); + return headlessSession; + } + } } diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java index 788a960658..f238433222 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/impl/WebServiceRM.java @@ -17,7 +17,6 @@ package io.cloudbeaver.service.rm.impl; import io.cloudbeaver.DBWebException; -import io.cloudbeaver.events.CBEventConstants; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.admin.AdminPermissionInfo; import io.cloudbeaver.service.rm.DBWServiceRM; @@ -33,9 +32,9 @@ import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMResource; import org.jkiss.dbeaver.model.security.*; +import org.jkiss.dbeaver.model.websocket.WSConstants; import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -111,17 +110,13 @@ public class WebServiceRM implements DBWServiceRM { checkIsRmEnabled(webSession); try { getResourceController(webSession).setResourceProperty(projectId, resourcePath, propertyName, propertyValue); - Map eventData = WebEventUtils.generateRmResourceEventData( + + WebEventUtils.addRmResourceUpdatedEvent( projectId, + webSession.getSessionId(), resourcePath, getResourceController(webSession).getResourcePath(projectId, resourcePath), - CBEventConstants.EventType.TYPE_UPDATE - ); - eventData.put("propertyName", propertyName); - eventData.put("propertyValue", propertyValue); - WebEventUtils.addRmResourceUpdatedEvent( - webSession.getSessionId(), - eventData + WSConstants.EventAction.UPDATE ); return Boolean.TRUE.toString(); } catch (DBException e) { @@ -158,7 +153,7 @@ public class WebServiceRM implements DBWServiceRM { webSession.getSessionId(), resourcePath, getResourceController(webSession).getResourcePath(projectId, resourcePath), - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); return result; } catch (Exception e) { @@ -180,7 +175,7 @@ public class WebServiceRM implements DBWServiceRM { webSession.getSessionId(), resourcePath, rmResourcePath, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); return true; } catch (Exception e) { @@ -206,14 +201,14 @@ public class WebServiceRM implements DBWServiceRM { webSession.getSessionId(), oldResourcePath, oldRmResourcePath, - CBEventConstants.EventType.TYPE_DELETE + WSConstants.EventAction.DELETE ); WebEventUtils.addRmResourceUpdatedEvent( projectId, webSession.getSessionId(), newResourcePath, newRmResourcePath, - CBEventConstants.EventType.TYPE_CREATE + WSConstants.EventAction.CREATE ); return true; } catch (Exception e) { @@ -234,9 +229,9 @@ public class WebServiceRM implements DBWServiceRM { try { byte[] bytes = data.getBytes(StandardCharsets.UTF_8); String content = getResourceController(webSession).setResourceContents(projectId, resourcePath, bytes, forceOverwrite); - var eventType = CBEventConstants.EventType.TYPE_UPDATE; + var eventType = WSConstants.EventAction.UPDATE; if (!forceOverwrite) { - eventType = CBEventConstants.EventType.TYPE_CREATE; + eventType = WSConstants.EventAction.CREATE; } WebEventUtils.addRmResourceUpdatedEvent( projectId, diff --git a/server/product/web-server/CloudbeaverServer.product.launch b/server/product/web-server/CloudbeaverServer.product.launch index 640feb6e01..6cdc3719ff 100644 --- a/server/product/web-server/CloudbeaverServer.product.launch +++ b/server/product/web-server/CloudbeaverServer.product.launch @@ -45,7 +45,6 @@ - @@ -81,6 +80,7 @@ + @@ -95,6 +95,7 @@ + diff --git a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF index 60b470f15f..4e4182404c 100644 --- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF +++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF @@ -18,6 +18,7 @@ Require-Bundle: org.eclipse.core.runtime, org.jkiss.dbeaver.ext.generic, org.jkiss.dbeaver.ext.h2, org.jkiss.dbeaver.ext.postgresql, + org.jkiss.bundle.jetty.websocket, io.cloudbeaver.model, io.cloudbeaver.server, io.cloudbeaver.resources.drivers.base, diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreen.tsx b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreen.tsx index 95cf02a3f7..69659cb43e 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreen.tsx +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardScreen.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; -import { Translate, useDataResource, usePermission } from '@cloudbeaver/core-blocks'; +import { Translate, useResource, usePermission } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { SessionPermissionsResource } from '@cloudbeaver/core-root'; @@ -21,7 +21,7 @@ import { WizardStepper } from './WizardStepper'; import { WizardTopAppBar } from './WizardTopAppBar/WizardTopAppBar'; export const ConfigurationWizardScreen = observer(function ConfigurationWizardScreen() { - useDataResource(ConfigurationWizardScreen, SessionPermissionsResource, undefined); + useResource(ConfigurationWizardScreen, SessionPermissionsResource, undefined); const administrationItemService = useService(AdministrationItemService); const administrationScreenService = useService(AdministrationScreenService); diff --git a/webapp/packages/core-app/src/Body.tsx b/webapp/packages/core-app/src/Body.tsx index 0b1266ed08..6e763e2fb1 100644 --- a/webapp/packages/core-app/src/Body.tsx +++ b/webapp/packages/core-app/src/Body.tsx @@ -10,11 +10,11 @@ import { observer } from 'mobx-react-lite'; import { useRef, useLayoutEffect } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, useAppLoadingScreen, useDataResource, useStyles } from '@cloudbeaver/core-blocks'; +import { Loader, useAppLoadingScreen, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { DialogsPortal } from '@cloudbeaver/core-dialogs'; import { Notifications } from '@cloudbeaver/core-notifications'; -import { SessionPermissionsResource } from '@cloudbeaver/core-root'; +import { ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; import { ThemeService } from '@cloudbeaver/core-theming'; import { DNDProvider } from '@cloudbeaver/core-ui'; @@ -39,10 +39,11 @@ const loaderStyle = css` export const Body = observer(function Body() { useAppLoadingScreen(); + // const serverConfigLoader = useResource(Body, ServerConfigResource, undefined); const themeService = useService(ThemeService); const style = useStyles(bodyStyles); const ref = useRef(null); - const permissionsService = useDataResource(Body, SessionPermissionsResource, undefined); + const permissionsService = useResource(Body, SessionPermissionsResource, undefined); const screenService = useService(ScreenService); const Screen = screenService.screen?.component; const { backendVersion } = useAppVersion(); @@ -58,7 +59,7 @@ export const Body = observer(function Body() { return styled(style)( - {() => styled(style)( + {() => styled(style)( <> {Screen && } diff --git a/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts b/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts index a8f2e8f067..2a996b917c 100644 --- a/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts +++ b/webapp/packages/core-authentication/src/AuthConfigurationsResource.ts @@ -33,7 +33,7 @@ export class AuthConfigurationsResource private readonly graphQLService: GraphQLService, permissionsResource: SessionPermissionsResource, ) { - super([]); + super(new Map(), []); permissionsResource .require(this, EAdminPermission.admin) diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 979b45780a..0494677846 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -219,7 +219,10 @@ UserInfoIncludes return this.data?.configurationParameters[key]; } - protected async loader(key: void, includes?: string[]): Promise { + protected async loader( + key: void, + includes?: ReadonlyArray, + ): Promise { const { user } = await this.graphQLService.sdk.getActiveUser({ ...this.getDefaultIncludes(), ...this.getIncludesMap(key, includes), diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts similarity index 61% rename from webapp/packages/core-blocks/src/useMapResource.ts rename to webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index 8a710d297e..9e0e42bda6 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -6,51 +6,58 @@ * you may not use this file except in compliance with the License. */ -import { computed, observable, untracked } from 'mobx'; +import { computed, observable, toJS, untracked } from 'mobx'; import { useEffect, useState } from 'react'; import { IServiceConstructor, useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList } from '@cloudbeaver/core-sdk'; -import { ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; +import { CachedMapResource, CachedMapResourceGetter, CachedMapResourceValue, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, isResourceKeyList, CachedResourceData, CachedDataResourceGetter, CachedResource, CachedDataResource, CachedResourceParam, CachedResourceKey, CachedResourceContext } from '@cloudbeaver/core-sdk'; +import { ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver/core-utils'; -import { getComputed } from './getComputed'; -import { useObservableRef } from './useObservableRef'; +import { getComputed } from '../getComputed'; +import { useObservableRef } from '../useObservableRef'; + +interface KeyWithIncludes { + readonly key: TKey | null; + readonly includes: TIncludes; +} + +type ResourceData< + TResource extends CachedResource, + TKey, + TIncludes +> = TResource extends CachedDataResource + ? CachedResourceData + : CachedMapResourceLoader< + TKey, + CachedResourceKey, + CachedResourceData extends Map ? I : never, + TIncludes + > +; interface IActions< - TKeyArg extends ResourceKey>, - TResource extends CachedMapResource, + TResource extends CachedResource, + TKey, TIncludes > { active?: boolean; silent?: boolean; isActive?: (resource: TResource) => Promise | boolean; - onLoad?: (resource: TResource, key: TKeyArg | null) => Promise | boolean | void; - onData?: ( - data: CachedMapResourceLoader< - TKeyArg, - CachedMapResourceKey, - CachedMapResourceValue, - TIncludes - >, + onLoad?: ( resource: TResource, - prevData: CachedMapResourceLoader< - TKeyArg, - CachedMapResourceKey, - CachedMapResourceValue, - TIncludes - > | undefined, + key: CachedResourceParam | null + ) => Promise | boolean | void; + onData?: ( + data: ResourceData, + resource: TResource, + prevData: ResourceData | undefined, ) => Promise | any; onError?: (exception: Error) => void; - preload?: Array>; + preload?: ILoadableState[]; } -interface KeyWithIncludes { - key: TKey | null; - includes: TIncludes; -} - -interface IMapResourceState> extends ILoadableState { +interface IMapResourceState extends ILoadableState { outdated: boolean; loading: boolean; loaded: boolean; @@ -60,10 +67,7 @@ interface IMapResourceState> reload: () => void; } -interface IMapResourceListResult< - TResource extends CachedMapResource, - TIncludes -> extends IMapResourceState { +interface IMapResourceListResult extends IMapResourceState { data: CachedMapResourceListGetter< CachedMapResourceValue, TIncludes @@ -71,10 +75,7 @@ interface IMapResourceListResult< exception: Error[] | null; } -interface IMapResourceResult< - TResource extends CachedMapResource, - TIncludes -> extends IMapResourceState { +interface IMapResourceResult extends IMapResourceState { data: CachedMapResourceGetter< CachedMapResourceValue, TIncludes @@ -82,63 +83,51 @@ interface IMapResourceResult< exception: Error | null; } -export function useMapResource< - TResource extends CachedMapResource, - TIncludes extends CachedResourceIncludeArgs< - CachedMapResourceValue, - CachedMapResourceArguments - > = [] ->( - component: { name: string }, - ctor: IServiceConstructor | TResource, - keyObj: TResource extends any - ? CachedMapResourceKey | null | KeyWithIncludes, TIncludes> - : never, - actions?: TResource extends any ? IActions, TResource, TIncludes> : never -): IMapResourceResult; +interface IDataResourceResult extends IMapResourceState { + data: CachedDataResourceGetter, TIncludes>; + exception: Error | null; +} -export function useMapResource< - TResource extends CachedMapResource, - TIncludes extends CachedResourceIncludeArgs< - CachedMapResourceValue, - CachedMapResourceArguments - > = [] ->( - component: { name: string }, - ctor: IServiceConstructor | TResource, - keyObj: TResource extends any - ? ( - ResourceKeyList> - | null - | KeyWithIncludes>, TIncludes> +type TResult = ( + TResource extends CachedDataResource + ? IDataResourceResult + : ( + TKey extends ResourceKeyList + ? IMapResourceListResult + : IMapResourceResult ) - : never, - actions?: TResource extends any - ? IActions>, TResource, TIncludes> - : never -): IMapResourceListResult; +); -export function useMapResource< - TResource extends CachedMapResource, - TKeyArg extends ResourceKey>, - TIncludes extends CachedResourceIncludeArgs< - CachedMapResourceValue, - CachedMapResourceArguments - > = [] +export function useResource< + TResource extends CachedResource, + TKeyArg extends CachedResourceParam, + TIncludes extends Readonly> >( component: { name: string }, ctor: IServiceConstructor | TResource, keyObj: TResource extends any ? TKeyArg | null | KeyWithIncludes : never, - actions?: TResource extends any ? IActions : never -): IMapResourceResult | IMapResourceListResult { + actions?: TResource extends any ? IActions : never +): TResult; + +export function useResource< + TResource extends CachedResource, + TKeyArg extends CachedResourceParam, + TIncludes extends CachedResourceContext +>( + component: { name: string }, + ctor: IServiceConstructor | TResource, + keyObj: TResource extends any ? TKeyArg | null | KeyWithIncludes : never, + actions?: TResource extends any ? IActions : never +): IMapResourceResult + | IMapResourceListResult + | IDataResourceResult { // eslint-disable-next-line react-hooks/rules-of-hooks - const resource = ctor instanceof CachedMapResource ? ctor : useService(ctor); + const resource = ctor instanceof CachedResource ? ctor : useService(ctor); const notifications = useService(NotificationService); - const [use, setUse] = useState<[TKeyArg | null, string]>([null, '']); const [exception, setException] = useState(null); let key: TKeyArg | null = keyObj as TKeyArg; let includes: TIncludes = [] as unknown as TIncludes; - const [loadFunctionName] = useState(`${component.name}.useMapResource(${resource.getName()}).load`); + const [loadFunctionName] = useState(`${component.name}.useResource(${resource.getName()}).load`); if (isKeyWithIncludes(keyObj)) { key = keyObj.key; @@ -192,23 +181,27 @@ export function useMapResource< const refObj = useObservableRef(() => ({ loading: false, - prevData: undefined as CachedMapResourceLoader< - TKeyArg, - CachedMapResourceKey, - CachedMapResourceValue, - TIncludes - > | undefined, + prevData: (isResourceKeyList(key) ? [] : undefined) as unknown | undefined, + useRef: [null, ''] as [TKeyArg | null, string], + use(key: TKeyArg | null) { + key = toJS(key); + + if (this.useRef[0] !== null) { + if (key !== null && this.resource.includes(key, this.useRef[0])) { + return; + } + + resource.free(this.useRef[0], this.useRef[1]); + } + this.useRef = [key, key === null ? '' : resource.use(key)]; + }, get preloaded(): boolean { if (this.actions?.preload) { for (const preload of this.actions.preload) { if ( !preload.isLoaded() - || preload.isOutdated() - || ( - Array.isArray(preload.exception) - ? preload.exception.some(Boolean) - : !!preload.exception - ) + || preload.isOutdated?.() + || preload.isError() ) { return false; } @@ -217,11 +210,11 @@ export function useMapResource< return true; }, async [loadFunctionName](refresh?: boolean) { - const { resource, actions, prevData } = this; + const { loading, resource, actions, prevData } = this; const key = keyRef.key; const includes = keyRef.includes; - if (this.loading) { + if (loading) { return; } @@ -247,7 +240,6 @@ export function useMapResource< } const newData = await resource.load(key, includes as any); - setUse([key, resource.use(key)]); this.prevData = newData; await actions?.onData?.( @@ -315,10 +307,15 @@ export function useMapResource< return []; } - return undefined; + if (refObj.resource instanceof CachedMapResource) { + return undefined; + } } - return refObj.resource.get(keyRef.key); + if (refObj.resource instanceof CachedMapResource) { + return refObj.resource.get(keyRef.key); + } + return refObj.resource.data; }, get outdated() { if (keyRef.key === null || !refObj.preloaded) { @@ -332,11 +329,7 @@ export function useMapResource< return true; } - if ( - Array.isArray(this.exception) - ? this.exception.some(Boolean) - : !!this.exception - ) { + if (this.isError()) { return false; } @@ -349,6 +342,9 @@ export function useMapResource< return refObj.loading || refObj.resource.isDataLoading(keyRef.key); }, + isError() { + return isContainsException(this.exception); + }, isOutdated() { return this.outdated; }, @@ -381,19 +377,15 @@ export function useMapResource< && refObj.preloaded && keyRef.key !== null && actions?.active !== false - && ( - result.exception === null - || (Array.isArray(result.exception) && !result.exception.some(Boolean)) - ) + && !result.isError() )); useEffect(() => () => { - if (use[0] !== null) { - resource.free(use[0], use[1]); - } - }, [resource, use]); + refObj.use(null); + }, []); useEffect(() => { + refObj.use(key); if (canLoad) { (refObj as any)[loadFunctionName](); } diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 50ba81cde2..2dcee49074 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -162,12 +162,11 @@ export * from './useClickEvents'; export * from './useControlledScroll'; export * from './useClipboard'; export * from './useCombinedHandler'; -export * from './useDataResource'; export * from './useExecutor'; export * from './useFn'; export * from './useFocus'; export * from './useFormValidator'; -export * from './useMapResource'; +export * from './ResourcesHooks/useResource'; export * from './useMouse'; export * from './useObjectRef'; export * from './useObservableRef'; diff --git a/webapp/packages/core-blocks/src/useDataResource.ts b/webapp/packages/core-blocks/src/useDataResource.ts deleted file mode 100644 index f452661aa5..0000000000 --- a/webapp/packages/core-blocks/src/useDataResource.ts +++ /dev/null @@ -1,226 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2022 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, useState } from 'react'; - -import { IServiceConstructor, useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { CachedDataResource, CachedDataResourceContext, CachedDataResourceGetter, CachedDataResourceParam, CachedResourceData, CachedResourceIncludeArgs, isResourceKeyList } from '@cloudbeaver/core-sdk'; -import type { ILoadableState } from '@cloudbeaver/core-utils'; - -import { getComputed } from './getComputed'; -import { useObjectRef } from './useObjectRef'; - -interface KeyWithIncludes { - key: TKey | null; - includes: TIncludes; -} - -interface IActions< - TResource, - TKeyArg extends CachedDataResourceParam, -> { - active?: boolean; - silent?: boolean; - isActive?: (resource: TResource) => Promise | boolean; - onLoad?: (resource: TResource, key: TKeyArg | null) => Promise | any; - onData?: ( - data: CachedResourceData, - resource: TResource, - prevData: CachedResourceData | undefined, - ) => Promise | any; - onError?: (exception: Error) => void; -} - -interface IMapResourceResult< - TResource, - TIncludes -> extends ILoadableState { - data: CachedDataResourceGetter, TIncludes>; - resource: TResource; - exception: Error | null; - reload: () => void; - load: () => void; -} - -export function useDataResource< - TResource, - TKeyArg extends CachedDataResourceParam, - TIncludes extends CachedResourceIncludeArgs< - CachedResourceData, - CachedDataResourceContext - > = [] ->( - component: React.FC, - ctor: IServiceConstructor | TResource, - keyObj: TResource extends any - ? TKeyArg | null | KeyWithIncludes - : never, - actions?: IActions -): IMapResourceResult; - -export function useDataResource< - TResource extends CachedDataResource, - TKeyArg extends CachedDataResourceParam, - TIncludes extends CachedResourceIncludeArgs< - CachedResourceData, - CachedDataResourceContext - > = [] ->( - component: React.FC, - ctor: IServiceConstructor | TResource, - keyObj: TResource extends any - ? TKeyArg | null | KeyWithIncludes - : never, - actions?: IActions -): IMapResourceResult { - // eslint-disable-next-line react-hooks/rules-of-hooks - const resource = ctor instanceof CachedDataResource ? ctor : useService(ctor); - const notifications = useService(NotificationService); - const [exception, setException] = useState(null); - let key: TKeyArg | null = keyObj as TKeyArg; - let includes: TIncludes = [] as unknown as TIncludes; - const [loadFunctionName] = useState(`${component.name}.useDataResource(${resource.getName()}).load`); - - if (isKeyWithIncludes(keyObj)) { - key = keyObj.key; - includes = keyObj.includes; - } - - // TODO: getComputed skips update somehow ... - const outdated = ( - (resource.isOutdated(key) || !resource.isLoaded(key, includes as any)) - && !resource.isDataLoading(key) - ); - - const refObj = useObjectRef(() => ({ - loading: false, - firstRender: true, - prevData: (isResourceKeyList(key) ? [] : undefined) as CachedResourceData | undefined, - async [loadFunctionName](refresh?: boolean) { - const { key, includes, loading, resource, actions, prevData } = this; - - if (loading) { - return; - } - - try { - const active = await actions?.isActive?.(resource); - - if (active === false) { - return; - } - - this.firstRender = false; - this.loading = true; - - await actions?.onLoad?.(resource, key); - - if (key === null) { - return; - } - - if (refresh) { - resource.markOutdated(key); - } - - const newData = await resource.load(key, includes as any); - setException(null); - - try { - await actions?.onData?.( - newData, - resource, - prevData - ); - } finally { - this.prevData = newData; - } - } catch (exception: any) { - if (resource.getException(key) === null) { - setException(exception); - } - - actions?.onError?.(exception); - - if (!actions?.silent && !this.exceptionObserved) { - notifications.logException(exception, 'Can\'t load data'); - } - } finally { - this.loading = false; - } - }, - }), { - exceptionObserved: false, - resource, - key, - exception, - includes, - actions, - }); - - const [result] = useState>(() => ({ - get resource() { - return refObj.resource; - }, - get exception() { - refObj.exceptionObserved = true; - return refObj.exception || resource.getException(refObj.key); - }, - get data() { - return refObj.resource.data; - }, - isLoaded: () => { - if (refObj.key === null) { - return false; - } - - return refObj.resource.isLoaded(refObj.key, refObj.includes as any); - }, - reload: () => { - setException(null); - (refObj as any)[loadFunctionName](true); - }, - load: () => { - (refObj as any)[loadFunctionName](); - }, - isLoading: () => { - if (refObj.key === null) { - return false; - } - - if (refObj.key === undefined) { - return refObj.resource.isLoading(); - } - - return refObj.resource.isDataLoading(refObj.key); - }, - })); - - const canLoad = getComputed(() => ( - result.exception === null - && actions?.active !== false - && !((!outdated && !refObj.firstRender) || refObj.key === null) - )); - - useEffect(() => { - if (canLoad) { - (refObj as any)[loadFunctionName](); - } - }); - - // if (canLoad) { - // throw (refObj as any)[loadFunctionName](); - // } - - return result; -} - -function isKeyWithIncludes(obj: any): obj is KeyWithIncludes { - return obj && typeof obj === 'object' && 'includes' in obj && 'key' in obj; -} diff --git a/webapp/packages/core-blocks/src/usePromiseState.ts b/webapp/packages/core-blocks/src/usePromiseState.ts index 7233815984..c57cf062c4 100644 --- a/webapp/packages/core-blocks/src/usePromiseState.ts +++ b/webapp/packages/core-blocks/src/usePromiseState.ts @@ -10,7 +10,7 @@ import { computed, observable } from 'mobx'; import { useEffect } from 'react'; import { Task } from '@cloudbeaver/core-executor'; -import type { ILoadableState } from '@cloudbeaver/core-utils'; +import { ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; import { useObservableRef } from './useObservableRef'; @@ -25,6 +25,9 @@ export function usePromiseState(promise: Promise | null): ILoadableState { } return undefined; }, + isError() { + return isContainsException(this.exception); + }, isCancelled(): boolean { if (this.promise instanceof Task) { return this.promise.cancelled; diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index 9e0a006cfe..1fdba51efe 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -3,6 +3,7 @@ const { resolve } = require('path'); const webpack = require('webpack'); const httpProxy = require('http-proxy'); const fs = require('fs'); +const { URL } = require('url'); const commonConfig = require('./webpack.config.js'); const main = resolve('src/index.ts'); @@ -31,64 +32,72 @@ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) { }; } -module.exports = (env, argv) => merge(commonConfig(env, argv), { - entry: { - main, - sso, - }, - mode: 'development', - devServer: { - allowedHosts: 'all', - // port: 8080, - client: { - webSocketURL: 'auto://0.0.0.0:0/ws', +module.exports = (env, argv) => { + const urlObject = new URL(env.server); + + return merge(commonConfig(env, argv), { + entry: { + main, + sso, }, - server, - proxy: { - '/api': { - target: env.server, + mode: 'development', + devServer: { + allowedHosts: 'all', + // port: 8080, + client: { + webSocketURL: 'auto://0.0.0.0:0/ws', + }, + server, + proxy: { + '/api': { + target: env.server, + }, + '/api/ws': { + target: `ws://${urlObject.hostname}:${urlObject.port}/api/ws`, + ws: true, + }, + }, + onListening: function (devServer, ...args) { + if (!devServer) { + throw new Error('webpack-dev-server is not defined'); + } + const logger = devServer.compiler.getInfrastructureLogger('webpack-dev-server'); + const port = devServer.server.address().port; + logger.info(`Proxy from http://localhost:8080 to http://127.0.0.1:${port}`); + httpProxy.createProxyServer({ target:`http://127.0.0.1:${port}` }).listen(8080); }, }, - onListening: function (devServer, ...args) { - if (!devServer) { - throw new Error('webpack-dev-server is not defined'); - } - const logger = devServer.compiler.getInfrastructureLogger('webpack-dev-server'); - const port = devServer.server.address().port; - logger.info(`Proxy from http://localhost:8080 to http://127.0.0.1:${port}`); - httpProxy.createProxyServer({ target:`http://127.0.0.1:${port}` }).listen(8080); + devtool: 'eval-source-map', + optimization: { + moduleIds: 'named', }, - }, - devtool: 'eval-source-map', - optimization: { - moduleIds: 'named', - }, - plugins: [ - new CopyWebpackPlugin({ - patterns: getAssets(package, ''), - }), - new webpack.DefinePlugin({ - _VERSION_: JSON.stringify(package.version), - _DEV_: true, - }), - new HtmlWebpackPlugin({ - template: resolve('src/index.html.ejs'), - inject: 'body', - chunks: ['main'], - version: package.version, - title: package.product?.name, - }), - new HtmlWebpackPlugin({ - filename: 'sso.html', - template: ssoHtmlTemplate, - inject: 'body', - chunks: ['sso'], - version: package.version, - title: package.product?.name, - }), - new HtmlInjectWebpackPlugin({ - body: [{ attributes: { hidden: true }, tagName: 'object', innerHTML: '{STATIC_CONTENT}', voidTag: false }], - }), - new webpack.HotModuleReplacementPlugin(), - ], -}); + plugins: [ + new CopyWebpackPlugin({ + patterns: getAssets(package, ''), + }), + new webpack.DefinePlugin({ + _VERSION_: JSON.stringify(package.version), + _DEV_: true, + }), + new HtmlWebpackPlugin({ + template: resolve('src/index.html.ejs'), + inject: 'body', + chunks: ['main'], + version: package.version, + title: package.product?.name, + }), + new HtmlWebpackPlugin({ + filename: 'sso.html', + template: ssoHtmlTemplate, + inject: 'body', + chunks: ['sso'], + version: package.version, + title: package.product?.name, + }), + new HtmlInjectWebpackPlugin({ + body: [{ attributes: { hidden: true }, tagName: 'object', innerHTML: '{STATIC_CONTENT}', voidTag: false }], + }), + new webpack.HotModuleReplacementPlugin(), + ], + }); +}; diff --git a/webapp/packages/core-connections/src/ConnectionFolderEventHandler.ts b/webapp/packages/core-connections/src/ConnectionFolderEventHandler.ts index 79e52a14e9..8ceb4b8bfe 100644 --- a/webapp/packages/core-connections/src/ConnectionFolderEventHandler.ts +++ b/webapp/packages/core-connections/src/ConnectionFolderEventHandler.ts @@ -7,30 +7,21 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { type SessionEvent, SessionEventSource, SessionEventType } from '@cloudbeaver/core-root'; -import { CbEvent, CbEventStatus as EConnectionInfoEventType, ResourceEventHandler } from '@cloudbeaver/core-sdk'; +import { ISessionEvent, SessionEventId, SessionEventSource, SessionEventTopic, TopicEventHandler } from '@cloudbeaver/core-root'; +import type { CbDatasourceFolderEvent as IConnectionFolderEvent } from '@cloudbeaver/core-sdk'; -export { EConnectionInfoEventType }; - -export interface IConnectionFolderEvent { - eventType: EConnectionInfoEventType; - nodePaths: string[]; - projectId: string; -} +export { IConnectionFolderEvent }; @injectable() -export class ConnectionFolderEventHandler extends ResourceEventHandler { +export class ConnectionFolderEventHandler + extends TopicEventHandler { constructor( sessionEventSource: SessionEventSource ) { - super(sessionEventSource); + super(SessionEventTopic.CbDatasourceFolder, sessionEventSource); } - map(event: CbEvent): IConnectionFolderEvent { - return event.eventData; - } - - filter(event: CbEvent): boolean { - return event.eventType === SessionEventType.CbDatasourceFolderUpdated; + map(event: any): IConnectionFolderEvent { + return event; } } diff --git a/webapp/packages/core-connections/src/ConnectionInfoEventHandler.ts b/webapp/packages/core-connections/src/ConnectionInfoEventHandler.ts index 55070771ab..81d3dff36a 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoEventHandler.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoEventHandler.ts @@ -7,30 +7,21 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { type SessionEvent, SessionEventSource, SessionEventType } from '@cloudbeaver/core-root'; -import { CbEvent, CbEventStatus as EConnectionInfoEventType, ResourceEventHandler } from '@cloudbeaver/core-sdk'; +import { ISessionEvent, SessionEventId, SessionEventSource, SessionEventTopic, TopicEventHandler } from '@cloudbeaver/core-root'; +import type { CbDatasourceEvent as IConnectionInfoEvent } from '@cloudbeaver/core-sdk'; -export { EConnectionInfoEventType }; - -export interface IConnectionInfoEvent { - eventType: EConnectionInfoEventType; - dataSourceIds: string[]; - projectId: string; -} +export { IConnectionInfoEvent }; @injectable() -export class ConnectionInfoEventHandler extends ResourceEventHandler { +export class ConnectionInfoEventHandler + extends TopicEventHandler { constructor( sessionEventSource: SessionEventSource ) { - super(sessionEventSource); + super(SessionEventTopic.CbDatasource, sessionEventSource); } - map(event: CbEvent): IConnectionInfoEvent { - return event.eventData; - } - - filter(event: CbEvent): boolean { - return event.eventType === SessionEventType.CbDatasourceUpdated; + map(event: any): IConnectionInfoEvent { + return event; } } diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index e60b2874a5..eb4a415369 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -11,7 +11,7 @@ import { action, makeObservable, observable, runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { SyncExecutor, ExecutorInterrupter, ISyncExecutor } from '@cloudbeaver/core-executor'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { EPermission, NavigatorViewSettings, SessionPermissionsResource, SessionDataResource, DataSynchronizationService } from '@cloudbeaver/core-root'; +import { EPermission, NavigatorViewSettings, SessionPermissionsResource, SessionDataResource, DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root'; import { GraphQLService, CachedMapResource, @@ -32,7 +32,7 @@ import { isResourceKeyList, } from '@cloudbeaver/core-sdk'; -import { ConnectionInfoEventHandler, EConnectionInfoEventType, IConnectionInfoEvent } from './ConnectionInfoEventHandler'; +import { ConnectionInfoEventHandler, IConnectionInfoEvent } from './ConnectionInfoEventHandler'; import type { DatabaseConnection } from './DatabaseConnection'; import type { IConnectionInfoParams } from './IConnectionsResource'; @@ -107,19 +107,24 @@ export class ConnectionInfoResource this.markOutdated(); }); - connectionInfoEventHandler - .on>( + connectionInfoEventHandler.onEvent>( + ServerEventId.CbDatasourceCreated, async key => { const connections = await this.load(key); for (const connection of connections) { this.onConnectionCreate.execute(connection); } - }, data => resourceKeyList(data.dataSourceIds.map(connectionId => ({ + }, + data => resourceKeyList(data.dataSourceIds.map(connectionId => ({ projectId: data.projectId, connectionId, - }))), d => d.eventType === EConnectionInfoEventType.TypeCreate) - .on>( + }))), + this + ); + + connectionInfoEventHandler.onEvent>( + ServerEventId.CbDatasourceUpdated, key => { if (this.isConnected(key)) { const connection = this.get(key); @@ -134,11 +139,16 @@ export class ConnectionInfoResource } else { this.markOutdated(key); } - }, data => resourceKeyList(data.dataSourceIds.map(connectionId => ({ + }, + data => resourceKeyList(data.dataSourceIds.map(connectionId => ({ projectId: data.projectId, connectionId, - }))), d => d.eventType === EConnectionInfoEventType.TypeUpdate) - .on( + }))), + this + ); + + connectionInfoEventHandler.onEvent( + ServerEventId.CbDatasourceDeleted, data => { const key = resourceKeyList(data.dataSourceIds.map(connectionId => ({ projectId: data.projectId, @@ -157,7 +167,10 @@ export class ConnectionInfoResource } else { this.delete(key); } - }, d => d, d => d.eventType === EConnectionInfoEventType.TypeDelete); + }, + undefined, + this + ); makeObservable(this, { nodeIdMap: observable, diff --git a/webapp/packages/core-connections/src/ContainerResource.ts b/webapp/packages/core-connections/src/ContainerResource.ts index dae6a1613f..c800250ff7 100644 --- a/webapp/packages/core-connections/src/ContainerResource.ts +++ b/webapp/packages/core-connections/src/ContainerResource.ts @@ -126,7 +126,11 @@ string return container.activeCatalog === (catalogId ?? defaultCatalog); } - isOutdated(param: ObjectContainerParams): boolean { + isOutdated(param?: ObjectContainerParams): boolean { + if (!param) { + return super.isOutdated(); + } + const metadata = this.metadata.get(serializeKey(param)); const catalogId = param.catalogId ?? defaultCatalog; return metadata.outdatedData.includes(catalogId); @@ -153,7 +157,12 @@ string metadata.loadingData = metadata.loadingData.filter(id => id !== catalogId); } - markOutdated(param: ObjectContainerParams): void { + markOutdated(param?: ObjectContainerParams): void { + if (!param) { + super.markOutdated(); + return; + } + const catalogId = param.catalogId ?? defaultCatalog; const metadata = this.metadata.get(serializeKey(param)); diff --git a/webapp/packages/core-connections/src/DBDriverResource.ts b/webapp/packages/core-connections/src/DBDriverResource.ts index 5b749c451f..f951e0daba 100644 --- a/webapp/packages/core-connections/src/DBDriverResource.ts +++ b/webapp/packages/core-connections/src/DBDriverResource.ts @@ -59,7 +59,7 @@ export class DBDriverResource extends CachedMapResource, includes: string[]): Promise> { + protected async loader(key: ResourceKey, includes?: ReadonlyArray): Promise> { const all = ResourceKeyUtils.includes(key, CachedMapAllKey); key = this.transformParam(key); diff --git a/webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts b/webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts index adeba9e7ef..110e2ab231 100644 --- a/webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts +++ b/webapp/packages/core-connections/src/NavTree/ConnectionNavNodeService.ts @@ -12,7 +12,8 @@ import { Dependency, injectable } from '@cloudbeaver/core-di'; import { ExecutorInterrupter, IAsyncContextLoader, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { INodeNavigationData, NavigationType, NavNodeInfoResource, NavNodeManagerService, NavTreeResource, NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; import { getProjectNodeId } from '@cloudbeaver/core-projects'; -import { type ResourceKey, ResourceKeyUtils, resourceKeyList, CachedMapAllKey, CbEventStatus } from '@cloudbeaver/core-sdk'; +import { ServerEventId } from '@cloudbeaver/core-root'; +import { type ResourceKey, ResourceKeyUtils, resourceKeyList, CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { ConnectionFolderEventHandler, IConnectionFolderEvent } from '../ConnectionFolderEventHandler'; import { Connection, ConnectionInfoResource, createConnectionParam } from '../ConnectionInfoResource'; @@ -48,7 +49,10 @@ export class ConnectionNavNodeService extends Dependency { this.navNodeManagerService.navigator.addHandler(this.navigateHandler.bind(this)); - this.connectionFolderEventHandler.on( + this.connectionInfoResource.connect(this.navTreeResource); + + this.connectionFolderEventHandler.onEvent( + ServerEventId.CbDatasourceFolderCreated, data => { const parents = data.nodePaths.map(nodeId => { const parents = getFolderNodeParents(nodeId); @@ -57,9 +61,11 @@ export class ConnectionNavNodeService extends Dependency { }); this.navTreeResource.markTreeOutdated(resourceKeyList(parents)); }, - v => v, - event => event.eventType === CbEventStatus.TypeCreate - ).on( + undefined, + this.navTreeResource + ); + this.connectionFolderEventHandler.onEvent( + ServerEventId.CbDatasourceFolderDeleted, data => { const parents = data.nodePaths.map(nodeId => { const parents = getFolderNodeParents(nodeId); @@ -69,14 +75,16 @@ export class ConnectionNavNodeService extends Dependency { this.navTreeResource.deleteInNode(resourceKeyList(parents), data.nodePaths); }, - v => v, - event => event.eventType === CbEventStatus.TypeDelete - ).on( + undefined, + this.navTreeResource + ); + this.connectionFolderEventHandler.onEvent( + ServerEventId.CbDatasourceFolderUpdated, data => { this.navTreeResource.markOutdated(resourceKeyList(data.nodePaths)); }, - v => v, - event => event.eventType === CbEventStatus.TypeUpdate + undefined, + this.navTreeResource ); } @@ -111,7 +119,11 @@ export class ConnectionNavNodeService extends Dependency { } } - private connectionUpdateHandler(key: ResourceKey) { + private connectionUpdateHandler(key: ResourceKey | undefined) { + if (key === undefined) { + key = CachedMapAllKey; + } + key = this.connectionInfoResource.transformParam(key); const outdatedTrees: string[] = []; const closedConnections: string[] = []; diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts index 44da87a066..a5be5712c4 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DBObjectResource.ts @@ -80,7 +80,12 @@ export class DBObjectResource extends CachedMapResource { return objectInfo; } - private outdateChildren(key: ResourceKey): void { + private outdateChildren(key: ResourceKey | undefined): void { + if (!key) { + this.markOutdated(); + return; + } + const childrenToOutdate: string[] = []; ResourceKeyUtils.forEach(key, key => { diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts index 636a150946..b0d639d325 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts @@ -42,13 +42,6 @@ export class NavNodeInfoResource extends CachedMapResource { permissionsResource: SessionPermissionsResource, ) { super(); - permissionsResource.require(this, EPermission.public); - - makeObservable(this, { - setDetails: action, - updateNode: action, - setParent: action, - }); this.metadata = new MetadataMap(() => ({ outdated: true, @@ -58,6 +51,14 @@ export class NavNodeInfoResource extends CachedMapResource { includes: observable([]), dependencies: observable([]), })); + + makeObservable(this, { + setDetails: action, + updateNode: action, + setParent: action, + }); + + permissionsResource.require(this, EPermission.public); } updateNode(key: string, node: NavNode): void; diff --git a/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts b/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts new file mode 100644 index 0000000000..2ed680dd4a --- /dev/null +++ b/webapp/packages/core-projects/src/ProjectInfoEventHandler.ts @@ -0,0 +1,44 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { injectable } from '@cloudbeaver/core-di'; +import { SessionEventSource, TopicEventHandler, ISessionEvent, ClientEventId, SessionEventTopic, IBaseServerEvent, SessionEventId } from '@cloudbeaver/core-root'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; + +export type IProjectInfoEvent = IBaseServerEvent; + +@injectable() +export class ProjectInfoEventHandler + extends TopicEventHandler { + private lastActiveProjects: string[]; + + constructor( + sessionEventSource: SessionEventSource + ) { + super(SessionEventTopic.CbProjects, sessionEventSource); + this.lastActiveProjects = []; + + this.onInit.addHandler(() => { + this.lastActiveProjects = []; + }); + } + + setActiveProjects(projects: string[]): void { + console.log(projects); + if (isArraysEqual(this.lastActiveProjects, projects)) { + return; + } + + this.emit({ id: ClientEventId.CbClientProjectsActive, projects }); + this.lastActiveProjects = projects; + } + + map(event: ISessionEvent): IProjectInfoEvent { + return event; + } +} diff --git a/webapp/packages/core-projects/src/ProjectInfoResource.ts b/webapp/packages/core-projects/src/ProjectInfoResource.ts index 77ffab6bed..9a09fcc91a 100644 --- a/webapp/packages/core-projects/src/ProjectInfoResource.ts +++ b/webapp/packages/core-projects/src/ProjectInfoResource.ts @@ -24,7 +24,7 @@ export class ProjectInfoResource extends CachedMapResource private readonly userInfoResource: UserInfoResource, sessionPermissionsResource: SessionPermissionsResource ) { - super([]); + super(new Map(), []); this.sync(this.userInfoResource); sessionPermissionsResource.require(this, EPermission.public); diff --git a/webapp/packages/core-projects/src/ProjectsService.ts b/webapp/packages/core-projects/src/ProjectsService.ts index c8db5d901b..595ef144aa 100644 --- a/webapp/packages/core-projects/src/ProjectsService.ts +++ b/webapp/packages/core-projects/src/ProjectsService.ts @@ -9,13 +9,14 @@ import { computed, makeObservable } from 'mobx'; import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication'; -import { injectable } from '@cloudbeaver/core-di'; +import { Dependency, injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-sdk'; import { NavigationService } from '@cloudbeaver/core-ui'; import { isArraysEqual } from '@cloudbeaver/core-utils'; import { activeProjectsContext } from './activeProjectsContext'; +import { ProjectInfoEventHandler } from './ProjectInfoEventHandler'; import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource'; interface IActiveProjectData { @@ -27,7 +28,7 @@ interface IProjectsUserSettings { } @injectable() -export class ProjectsService { +export class ProjectsService extends Dependency { get userProject(): ProjectInfo | undefined { let project: ProjectInfo | undefined; @@ -92,13 +93,37 @@ export class ProjectsService { private readonly projectInfoResource: ProjectInfoResource, private readonly userInfoResource: UserInfoResource, private readonly userDataService: UserDataService, + private readonly projectInfoEventHandler: ProjectInfoEventHandler, navigationService: NavigationService ) { + super(); this.getActiveProjectTask = new SyncExecutor(); this.onActiveProjectChange = new Executor(); this.onActiveProjectChange.before(navigationService.navigationTask); + this.userInfoResource.onUserChange.addHandler(() => { + this.onActiveProjectChange.execute({ + type: 'after', + }); + }); + + this.projectInfoResource.onDataUpdate.addHandler(() => { + this.onActiveProjectChange.execute({ + type: 'after', + }); + }); + + this.onActiveProjectChange.addHandler(data => { + if (data.type === 'after') { + this.projectInfoEventHandler.setActiveProjects(this.activeProjects.map(project => project.id)); + } + }); + + this.projectInfoEventHandler.onInit.addHandler(() => { + this.projectInfoEventHandler.setActiveProjects(this.activeProjects.map(project => project.id)); + }); + makeObservable(this, { userProject: computed, defaultProject: computed, diff --git a/webapp/packages/core-projects/src/manifest.ts b/webapp/packages/core-projects/src/manifest.ts index 82ede7f825..d0c8ece929 100644 --- a/webapp/packages/core-projects/src/manifest.ts +++ b/webapp/packages/core-projects/src/manifest.ts @@ -9,6 +9,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; +import { ProjectInfoEventHandler } from './ProjectInfoEventHandler'; import { ProjectInfoResource } from './ProjectInfoResource'; import { ProjectsService } from './ProjectsService'; @@ -21,5 +22,6 @@ export const manifest: PluginManifest = { ProjectInfoResource, ProjectsService, LocaleService, + ProjectInfoEventHandler, ], }; diff --git a/webapp/packages/core-resource-manager/src/ResourceManagerEventHandler.ts b/webapp/packages/core-resource-manager/src/ResourceManagerEventHandler.ts index 9b9e443172..87b21979b1 100644 --- a/webapp/packages/core-resource-manager/src/ResourceManagerEventHandler.ts +++ b/webapp/packages/core-resource-manager/src/ResourceManagerEventHandler.ts @@ -7,30 +7,19 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { type SessionEvent, SessionEventSource, SessionEventType } from '@cloudbeaver/core-root'; -import { CbEvent, CbEventStatus as EResourceManagerEventType, ResourceEventHandler } from '@cloudbeaver/core-sdk'; - -export { EResourceManagerEventType }; - -export interface IConnectionInfoEvent { - eventType: EResourceManagerEventType; - resourcePath: string; - projectId: string; -} +import { ISessionEvent, SessionEventSource, TopicEventHandler, SessionEventTopic, SessionEventId } from '@cloudbeaver/core-root'; +import type { CbrmEvent as IResourceManagerEvent } from '@cloudbeaver/core-sdk'; @injectable() -export class ResourceManagerEventHandler extends ResourceEventHandler { +export class ResourceManagerEventHandler + extends TopicEventHandler { constructor( sessionEventSource: SessionEventSource ) { - super(sessionEventSource); + super(SessionEventTopic.CbScripts, sessionEventSource); } - map(event: CbEvent): IConnectionInfoEvent { - return event.eventData; - } - - filter(event: CbEvent): boolean { - return event.eventType === SessionEventType.CbRmResourceUpdated; + map(event: any): IResourceManagerEvent { + return event; } } diff --git a/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts b/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts index f32f2aebf8..e65f440cd6 100644 --- a/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts +++ b/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts @@ -10,11 +10,11 @@ import { observable, toJS } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -import { DataSynchronizationService } from '@cloudbeaver/core-root'; -import { CachedMapResource, CachedResourceIncludeArgs, GetResourceListQueryVariables, GraphQLService, ICachedMapResourceMetadata, isResourceKeyList, ResourceKey, ResourceKeyList, resourceKeyList, ResourceKeyUtils, RmResource } from '@cloudbeaver/core-sdk'; +import { DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root'; +import { CachedMapResource, CachedResourceIncludeArgs, GetResourceListQueryVariables, GraphQLService, ICachedMapResourceMetadata, isResourceKeyList, ResourceKey, resourceKeyList, ResourceKeyUtils, RmResource } from '@cloudbeaver/core-sdk'; import { createPath, isValuesEqual, MetadataMap } from '@cloudbeaver/core-utils'; -import { EResourceManagerEventType, ResourceManagerEventHandler } from './ResourceManagerEventHandler'; +import { ResourceManagerEventHandler } from './ResourceManagerEventHandler'; export type ResourceInfoIncludes = Omit; export type RmResourceInfo = RmResource; @@ -42,8 +42,8 @@ export class ResourceManagerResource this.onMove = new Executor(); - resourceManagerEventHandler - .on( + resourceManagerEventHandler.onEvent( + ServerEventId.CbDatasourceCreated, async key => { let parentFolderKey = createParentResourceKey(key); parentFolderKey = { @@ -71,12 +71,15 @@ export class ResourceManagerResource path: this.getFolder(data.resourcePath), name: this.getResourceName(data.resourcePath), }), - d => d.eventType === EResourceManagerEventType.TypeCreate) - .on( + this + ); + + resourceManagerEventHandler.onEvent( + ServerEventId.CbRmResourceUpdated, key => { if (this.isInUse(key)) { dataSynchronizationService - .requestSynchronization('resource', createPath(key.path, key?.name)) + .requestSynchronization('resource', createPath(key.path, key.name)) .then(state => { if (state) { this.onDataUpdate.execute(key); @@ -93,12 +96,15 @@ export class ResourceManagerResource path: this.getFolder(data.resourcePath), name: this.getResourceName(data.resourcePath), }), - d => d.eventType === EResourceManagerEventType.TypeUpdate) - .on( + this + ); + + resourceManagerEventHandler.onEvent( + ServerEventId.CbRmResourceDeleted, key => { if (this.isInUse(key)) { dataSynchronizationService - .requestSynchronization('resource', createPath(key.path, key?.name)) + .requestSynchronization('resource', createPath(key.path, key.name)) .then(state => { if (state) { this.delete(key); @@ -115,8 +121,8 @@ export class ResourceManagerResource path: this.getFolder(data.resourcePath), name: this.getResourceName(data.resourcePath), }), - d => d.eventType === EResourceManagerEventType.TypeDelete); - + this + ); } getResourceName(resourcePath: string): string { diff --git a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts index 186090eab8..db9c33de8b 100644 --- a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts +++ b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts @@ -33,7 +33,7 @@ export class SharedProjectsResource extends CachedMapResource { +export class ServerConfigEventHandler extends TopicEventHandler { constructor( sessionEventSource: SessionEventSource ) { - super(sessionEventSource); + super(SessionEventTopic.CbConfig, sessionEventSource); } - map(event: CbEvent): null { - return null; - } - - filter(event: CbEvent): boolean { - return event.eventType === SessionEventType.CbConfigChanged; + map(event: any) { + return event; } } diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index 71f73dae56..251e3ac586 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -29,7 +29,7 @@ export class ServerConfigResource extends CachedDataResource { if (state) { @@ -58,7 +58,7 @@ export class ServerConfigResource extends CachedDataResource undefined); + }, () => undefined, undefined, this); } get redirectOnFederatedAuth(): boolean { diff --git a/webapp/packages/core-root/src/ServerEventEmitter/IServerEventEmitter.ts b/webapp/packages/core-root/src/ServerEventEmitter/IServerEventEmitter.ts new file mode 100644 index 0000000000..6ee09fd770 --- /dev/null +++ b/webapp/packages/core-root/src/ServerEventEmitter/IServerEventEmitter.ts @@ -0,0 +1,47 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { Observable } from 'rxjs'; + +import type { ISyncExecutor } from '@cloudbeaver/core-executor'; + +export type IServerEventCallback = (data: T) => any; +export type Subscription = () => void; + +export interface IBaseServerEvent { + id: TID; + topicId?: TTopic; +} + +export interface IServerEventEmitter< + TEvent extends IBaseServerEvent, + TSourceEvent extends IBaseServerEvent = TEvent, + TEventID extends string = string, + TTopic extends string = string +> { + readonly onInit: ISyncExecutor; + + onEvent( + id: TEventID, + callback: IServerEventCallback, + mapTo?: (event: TEvent) => T, + ): Subscription; + + on( + callback: IServerEventCallback, + mapTo?: (event: TEvent) => T, + filter?: (event: TEvent) => boolean, + ): Subscription; + + multiplex( + topicId: TTopic, + mapTo?: (event: TEvent) => T, + ): Observable; + + emit(event: TSourceEvent): this; +} \ No newline at end of file diff --git a/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts b/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts new file mode 100644 index 0000000000..26fc172f70 --- /dev/null +++ b/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts @@ -0,0 +1,197 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { Connectable, connectable, filter, map, merge, Observable, Subject } from 'rxjs'; + +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import type { CachedResource } from '@cloudbeaver/core-sdk'; +import { compose } from '@cloudbeaver/core-utils'; + +import type { IBaseServerEvent, IServerEventCallback, IServerEventEmitter, Subscription } from './IServerEventEmitter'; + +interface ISubscribedResourceInfo { + listeners: number; + subscription: Subscription; +} + +export abstract class TopicEventHandler< + TEvent extends IBaseServerEvent, + SourceEvent extends IBaseServerEvent, + TEventID extends string = string, + TTopic extends string = string +> +implements IServerEventEmitter { + readonly onInit: ISyncExecutor; + readonly eventsSubject: Connectable; + + private subscription: Subscription | null; + private readonly activeResources: Array>; + private readonly subscribedResources: Map, ISubscribedResourceInfo>; + private readonly serverSubject?: Observable; + private readonly subject: Subject; + constructor( + private readonly topic: string, + private readonly emitter: IServerEventEmitter + ) { + this.onInit = new SyncExecutor(); + this.subject = new Subject(); + this.activeResources = []; + this.subscribedResources = new Map(); + this.subscription = null; + this.serverSubject = this.emitter.multiplex(topic, this.map); + this.eventsSubject = connectable(merge(this.subject, this.serverSubject)); + + this.emitter.onInit.next(this.onInit); + } + + multiplex( + topicId: TTopic, + mapTo: ((event: TEvent) => T) = event => event as unknown as T, + ): Observable { + return this.emitter.multiplex( + topicId, + compose(mapTo, this.map) as unknown as (event: SourceEvent) => T + ); + } + + onEvent( + id: TEventID, + callback: IServerEventCallback, + mapTo: (event: TEvent) => T = event => event as unknown as T, + resource?: CachedResource, + ): Subscription { + if (resource) { + this.registerResource(resource); + } + + const sub = this.eventsSubject + .pipe(filter(e => e.id === id), map(mapTo)) + .subscribe(callback); + + return () => { + sub.unsubscribe(); + + if (resource) { + this.removeResource(resource); + } + }; + } + + on( + callback: IServerEventCallback, + mapTo?: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + resource?: CachedResource, + ): Subscription; + on( + resource: CachedResource, + mapTo?: (param: TEvent) => T, + filter?: (param: TEvent) => boolean, + ): Subscription; + on( + resourceOrCallback: CachedResource | IServerEventCallback, + mapTo: (param: TEvent) => T = event => event as unknown as T, + filterFn: (param: TEvent) => boolean = () => true, + resource?: CachedResource, + ): Subscription { + let handler: IServerEventCallback; + + if (typeof resourceOrCallback === 'function') { + handler = resourceOrCallback; + } else { + resource = resourceOrCallback; + handler = event => { + resourceOrCallback.markOutdated(event); + }; + } + + if (resource) { + this.registerResource(resource); + } + + const sub = this.eventsSubject + .pipe(filter(filterFn), map(mapTo)) + .subscribe(handler); + + return () => { + sub.unsubscribe(); + + if (resource) { + this.removeResource(resource); + } + }; + } + + emit(event: SourceEvent): this { + this.emitter.emit(event); + return this; + } + + private resourceUseHandler(resource: CachedResource) { + const index = this.activeResources.indexOf(resource); + + if (index !== -1) { + if (!resource.isResourceInUse) { + this.removeActiveResource(resource); + } + } else { + if (resource.isResourceInUse) { + this.activeResources.push(resource); + + if (!this.subscription) { + // console.log('Subscribe: ', resource.getName()); + const sub = this.eventsSubject.connect(); + this.subscription = () => sub.unsubscribe(); + } + } + } + } + + private removeActiveResource(resource: CachedResource) { + this.activeResources.splice(this.activeResources.indexOf(resource), 1); + + if (this.activeResources.length === 0) { + // console.log('Unsubscribe: ', resource.getName()); + this.subscription?.(); + this.subscription = null; + } + } + + private registerResource(resource: CachedResource): void { + let info = this.subscribedResources.get(resource); + + if (!info) { + info = { + listeners: 0, + subscription: this.resourceUseHandler.bind(this, resource), + }; + this.subscribedResources.set(resource, info); + resource.onUse.addHandler(info.subscription); + // console.log('Register: ', resource.getName()); + } + + info.listeners++; + } + + private removeResource(resource: CachedResource): void { + const info = this.subscribedResources.get(resource); + + if (info) { + info.listeners--; + + if (info.listeners === 0) { + this.removeActiveResource(resource); + resource.onUse.removeHandler(info.subscription); + this.subscribedResources.delete(resource); + // console.log('Unregister: ', resource.getName()); + } + } + } + + protected abstract map(event: SourceEvent): TEvent; +} \ No newline at end of file diff --git a/webapp/packages/core-root/src/SessionEventSource.ts b/webapp/packages/core-root/src/SessionEventSource.ts index 327a760520..c03b57efdc 100644 --- a/webapp/packages/core-root/src/SessionEventSource.ts +++ b/webapp/packages/core-root/src/SessionEventSource.ts @@ -6,31 +6,151 @@ * you may not use this file except in compliance with the License. */ +import { catchError, debounceTime, filter, map, merge, Observable, retry, RetryConfig, Subject } from 'rxjs'; +import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; + import { injectable } from '@cloudbeaver/core-di'; +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { GraphQLService, - EventSource, - CbEvent, - CbEventType as SessionEventType + EnvironmentService, + CbEventTopic as SessionEventTopic, + CbServerEventId as ServerEventId, + CbClientEventId as ClientEventId, + ServiceError, } from '@cloudbeaver/core-sdk'; -export type SessionEvent = CbEvent; -export { SessionEventType }; +import type { IBaseServerEvent, IServerEventCallback, IServerEventEmitter, Subscription } from './ServerEventEmitter/IServerEventEmitter'; + +export { ServerEventId, SessionEventTopic, ClientEventId }; + +export type SessionEventId = ServerEventId | ClientEventId; + +export interface ISessionEvent extends IBaseServerEvent { + id: SessionEventId; + topicId?: SessionEventTopic; + [key: string]: any; +} + +export interface ITopicSubEvent extends ISessionEvent { + id: ClientEventId.CbClientTopicSubscribe | ClientEventId.CbClientTopicUnsubscribe; + topicId: SessionEventTopic; +} + +const retryInterval = 5000; + +const retryConfig: RetryConfig = { + delay: retryInterval, +}; @injectable() -export class SessionEventSource extends EventSource { +export class SessionEventSource +implements IServerEventEmitter { + readonly eventsSubject: Observable; + readonly onInit: ISyncExecutor; + + + private readonly closeSubject: Subject; + private readonly openSubject: Subject; + private readonly errorSubject: Subject; + private readonly subject: WebSocketSubject; + private readonly oldEventsSubject: Subject; + constructor( + private readonly environmentService: EnvironmentService, private readonly graphQLService: GraphQLService ) { - super(5000); - } - protected async listener(): Promise { - const { events } = await this.graphQLService.sdk.getSessionEvents({ - maxEntries: 1000, + this.onInit = new SyncExecutor(); + this.oldEventsSubject = new Subject(); + this.closeSubject = new Subject(); + this.openSubject = new Subject(); + this.errorSubject = new Subject(); + this.subject = webSocket({ + url: environmentService.wsEndpoint, + closeObserver: this.closeSubject, + openObserver: this.openSubject, }); - for (const event of events) { - this.event(event); - } + this.openSubject.subscribe(() => { + this.onInit.execute(); + }); + + this.closeSubject.subscribe(event => { + console.info(`Websocket closed: ${event.reason}`); + }); + + this.eventsSubject = merge(this.oldEventsSubject, this.subject); + + this.errorSubject + .pipe(debounceTime(1000)) + .subscribe(error => { + console.error(error); + }); + + this.errorHandler = this.errorHandler.bind(this); + } + + onEvent( + id: SessionEventId, + callback: IServerEventCallback, + mapTo: (event: ISessionEvent) => T = e => e as T, + ): Subscription { + const sub = this.eventsSubject + .pipe( + catchError(this.errorHandler), + filter(event => event.id === id), + map(mapTo) + ) + .subscribe(callback); + + return () => { + sub.unsubscribe(); + }; + } + + on( + callback: IServerEventCallback, + mapTo: (event: ISessionEvent) => T = e => e as T, + filterFn: (event: ISessionEvent) => boolean = () => true, + ): Subscription { + const sub = this.eventsSubject + .pipe( + catchError(this.errorHandler), + filter(filterFn), + map(mapTo) + ) + .subscribe(callback); + + return () => { + sub.unsubscribe(); + }; + } + + multiplex( + topicId: SessionEventTopic, + mapTo: ((event: ISessionEvent) => T) = e => e as T + ): Observable { + return merge( + this.subject.multiplex( + () => ({ id: ClientEventId.CbClientTopicSubscribe, topicId } as ITopicSubEvent), + () => ({ id: ClientEventId.CbClientTopicUnsubscribe, topicId } as ITopicSubEvent), + event => event.topicId === topicId + ), + this.oldEventsSubject, + ) + .pipe( + catchError(this.errorHandler), + map(mapTo) + ); + } + + emit(event: ISessionEvent): this { + this.subject.next(event); + return this; + } + + private errorHandler(error: any, caught: Observable): Observable { + this.errorSubject.next(new ServiceError('WebSocket connection error')); + return caught; } } diff --git a/webapp/packages/core-root/src/SessionPermissionsResource.ts b/webapp/packages/core-root/src/SessionPermissionsResource.ts index be348fc9f1..34c2c64018 100644 --- a/webapp/packages/core-root/src/SessionPermissionsResource.ts +++ b/webapp/packages/core-root/src/SessionPermissionsResource.ts @@ -23,7 +23,7 @@ export class SessionPermissionsResource extends CachedDataResource> this.sync(sessionDataResource, () => {}, () => {}); } - require(resource: CachedResource, ...permissions: string[]): this { + require(resource: CachedResource, ...permissions: string[]): this { resource .preloadResource(this, () => undefined) .before(ExecutorInterrupter.interrupter(() => !this.has(...permissions))); diff --git a/webapp/packages/core-root/src/getSocketIterable.ts b/webapp/packages/core-root/src/getSocketIterable.ts new file mode 100644 index 0000000000..631e733baf --- /dev/null +++ b/webapp/packages/core-root/src/getSocketIterable.ts @@ -0,0 +1,97 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { Socket } from 'socket.io-client'; + +export enum ESocketReservedEvents { + Connect = 'connect', + Disconnect = 'disconnect', + ConnectError = 'connect_error' +} + +export interface ISocketReservedEvent { + name: ESocketReservedEvents; + reason?: Socket.DisconnectReason; +} + +export type ISocketEventData = { + name: string; + data: T; +} | ISocketReservedEvent; + +type IEventResolver = (value: ISocketEventData | null, done: boolean) => void; + +export function getSocketIterable(socket: Socket) { + return new SocketIterable(socket); +} + +export class SocketIterable implements AsyncIterable | null> { + private readonly queue: IEventResolver[]; + + private subscribed: boolean; + constructor(private readonly socket: Socket) { + this.queue = []; + this.subscribed = false; + + this.subscription = this.subscription.bind(this); + } + + [Symbol.asyncIterator](): AsyncIterator | null> { + this.subscribe(); + return { + next: () => new Promise(resolve => { + this.queue.push((value, done) => { + resolve({ value, done }); + }); + }), + return: () => { + this.dispose(); + return Promise.resolve({ value: null, done: true }); + }, + throw: () => { + this.dispose(); + return Promise.resolve({ value: null, done: true }); + }, + }; + } + + private dispose() { + this.socket.offAny(this.subscription); + this.subscribed = false; + } + + private subscribe() { + if (this.subscribed) { + return; + } + this.socket.onAny(this.subscription); + this.subscribed = true; + } + + private subscription(name: string | ESocketReservedEvents, data?: T | Socket.DisconnectReason) { + while (this.queue.length > 0) { + const resolver = this.queue.shift(); + + if (resolver) { + if (isEventNameReserved(name)) { + resolver({ name, reason: data as Socket.DisconnectReason }, this.socket.active); + } else { + resolver({ name, data: data as T }, this.socket.active); + } + } + } + } +} + +function isEventNameReserved(name: string | ESocketReservedEvents): name is ESocketReservedEvents { + return ['connect', 'disconnect', 'connect_error'].includes(name); +} + +export function isEventReserved(event: ISocketEventData): event is ISocketReservedEvent { + return isEventNameReserved(event.name); +} \ No newline at end of file diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 6be3f3cd04..ee81d0218a 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -2,6 +2,8 @@ export * from './DataSynchronization/DataSynchronizationQueue'; export * from './DataSynchronization/DataSynchronizationService'; export * from './DataSynchronization/ISynchronizationMessage'; export * from './DataSynchronization/SynchronizationMessage'; +export * from './ServerEventEmitter/IServerEventEmitter'; +export * from './ServerEventEmitter/TopicEventHandler'; export * from './ConnectionNavigatorViewSettings'; export * from './FeaturesResource'; export * from './NetworkError'; diff --git a/webapp/packages/core-sdk/codegen.yml b/webapp/packages/core-sdk/codegen.yml index 96fea40a0a..e696982867 100644 --- a/webapp/packages/core-sdk/codegen.yml +++ b/webapp/packages/core-sdk/codegen.yml @@ -1,6 +1,7 @@ schema: - ../../../server/bundles/io.cloudbeaver.server/schema/schema.graphqls - ../../../server/bundles/io.cloudbeaver.server/schema/service.core.graphqls + - ../../../server/bundles/io.cloudbeaver.server/schema/service.events.graphqls - ../../../server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls - ../../../server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls - ../../../server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls diff --git a/webapp/packages/core-sdk/src/EnvironmentService.ts b/webapp/packages/core-sdk/src/EnvironmentService.ts index 6c23571ed6..156f090a9b 100644 --- a/webapp/packages/core-sdk/src/EnvironmentService.ts +++ b/webapp/packages/core-sdk/src/EnvironmentService.ts @@ -12,4 +12,5 @@ import { GlobalConstants } from '@cloudbeaver/core-utils'; @injectable() export class EnvironmentService { readonly gqlEndpoint = GlobalConstants.absoluteServiceUrl('gql'); + readonly wsEndpoint = `${GlobalConstants.wsProtocol}${GlobalConstants.hostServiceUrl('ws')}`; } diff --git a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts index 5340901afd..aef0c6274f 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts @@ -8,7 +8,7 @@ import { makeObservable, observable } from 'mobx'; -import type { ILoadableState } from '@cloudbeaver/core-utils'; +import { ILoadableState, isContainsException } from '@cloudbeaver/core-utils'; import { CachedResource } from './CachedResource'; import type { CachedResourceIncludeArgs, CachedResourceValueIncludes } from './CachedResourceIncludes'; @@ -22,9 +22,6 @@ export type CachedDataResourceParam = TResource extends CachedDataRes export type CachedDataResourceKey = TResource extends CachedDataResource ? T : never; -export type CachedDataResourceContext = TResource extends CachedDataResource - ? T - : never; export type CachedDataResourceGetter< TValue, @@ -35,23 +32,22 @@ export type CachedDataResourceGetter< : CachedResourceValueIncludes ); -type ContextArg = TContext extends void ? void : CachedResourceIncludeArgs; - export abstract class CachedDataResource< TData, TParam = void, TKey = TParam, - TContext extends Record | void = void, + TContext extends Record = Record > extends CachedResource< + TData, TData, TParam, TKey, - ContextArg + CachedResourceIncludeArgs > { protected loaded: boolean; - constructor(defaultValue: TData, defaultIncludes: ContextArg) { - super(defaultValue, defaultIncludes as any as string[]); + constructor(defaultValue: TData, defaultIncludes: CachedResourceIncludeArgs = [] as any) { + super(defaultValue, defaultIncludes); this.loaded = false; @@ -62,7 +58,7 @@ export abstract class CachedDataResource< this.onDataUpdate.addHandler(() => { this.loaded = true; }); } - isLoaded(param: TParam, includes: ContextArg): boolean { + isLoaded(param: TParam, includes?: CachedResourceIncludeArgs): boolean { if (!this.loaded) { return false; } @@ -79,16 +75,22 @@ export abstract class CachedDataResource< return true; } - async refresh(param: TParam, context: ContextArg): Promise { + async refresh>( + param: TParam, + context?: T + ): Promise> { await this.preLoadData(param, false, context); await this.loadData(param, true, context); - return this.data; + return this.data as CachedResourceValueIncludes; } - async load(param: TParam, context: ContextArg): Promise { + async load>( + param: TParam, + context?: T + ): Promise> { await this.preLoadData(param, false, context); await this.loadData(param, false, context); - return this.data; + return this.data as CachedResourceValueIncludes; } } @@ -96,11 +98,11 @@ export function getCachedDataResourceLoaderState< TData, TParam = void, TKey = TParam, - TContext extends Record | void = void, + TContext extends Record = Record, >( resource: CachedDataResource, param: TParam, - context: ContextArg + context?: CachedResourceIncludeArgs ): ILoadableState { return { get exception() { @@ -109,6 +111,9 @@ export function getCachedDataResourceLoaderState< isLoading() { return resource.isDataLoading(param); }, + isError() { + return isContainsException(this.exception); + }, isLoaded() { return resource.isLoaded(param, context); }, diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index ed5ad422b1..dec0f6f121 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -9,14 +9,14 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { ILoadableState, isArraysEqual, MetadataMap, uuid } from '@cloudbeaver/core-utils'; +import { ILoadableState, isArraysEqual, isContainsException, MetadataMap, uuid } from '@cloudbeaver/core-utils'; -import { CachedResource, CachedResourceKey, ICachedResourceMetadata } from './CachedResource'; +import { CachedResource, CachedResourceKey, CachedResourceParamKey, ICachedResourceMetadata } from './CachedResource'; import type { CachedResourceIncludeArgs, CachedResourceValueIncludes } from './CachedResourceIncludes'; import { ResourceKey, resourceKeyList, ResourceKeyList, ResourceKeyUtils } from './ResourceKeyList'; export type CachedMapResourceKey = CachedResourceKey; -export type CachedMapResourceValue = TResource extends CachedResource, any, any, any> +export type CachedMapResourceValue = TResource extends CachedResource, any, any, any, any> ? T : never; export type CachedMapResourceArguments = TResource extends CachedMapResource ? T : never; @@ -32,7 +32,7 @@ export type CachedMapResourceGetter< > = CachedResourceValueIncludes | undefined; export type CachedMapResourceLoader< - TRealKey extends ResourceKey, + TRealKey, TKey, TValue, TIncludes @@ -50,12 +50,13 @@ export const CachedMapEmptyKey = resourceKeyList([], 'empty'); export abstract class CachedMapResource< TKey, TValue, - TArguments = Record + TContext extends Record = Record > extends CachedResource< Map, + TValue, ResourceKey, TKey, - string[] | undefined + CachedResourceIncludeArgs > { readonly onItemAdd: ISyncExecutor>; readonly onItemDelete: ISyncExecutor>; @@ -69,8 +70,10 @@ export abstract class CachedMapResource< return Array.from(this.data.keys()); } - constructor(defaultIncludes?: CachedResourceIncludeArgs, defaultValue?: Map) { - //@ts-expect-error fix + constructor( + defaultValue?: Map, + defaultIncludes?: CachedResourceIncludeArgs + ) { super(defaultValue || new Map(), defaultIncludes); this.onItemAdd = new SyncExecutor>(null); this.onItemDelete = new SyncExecutor>(null); @@ -131,13 +134,11 @@ export abstract class CachedMapResource< return this; } - //@ts-expect-error fix - isIncludes(key: ResourceKey, includes: CachedResourceIncludeArgs): boolean { + isIncludes(key: ResourceKey, includes: CachedResourceIncludeArgs): boolean { key = this.transformParam(key); return ResourceKeyUtils.every(key, key => { const metadata = this.getMetadata(key); - //@ts-expect-error fix return includes.every(include => metadata.includes.includes(include)); }); } @@ -154,10 +155,16 @@ export abstract class CachedMapResource< return ResourceKeyUtils.every(key, key => super.isInUse(this.getMetadataKeyRef(key))); } - use(key: ResourceKey): string { + use(key: ResourceKey | typeof CachedResourceParamKey): string { + const id = uuid(); + + if (key === CachedResourceParamKey) { + super.use(CachedResourceParamKey, id); + return id; + } + key = this.transformParam(key); key = this.getLockedKeys(key); - const id = uuid(); ResourceKeyUtils.map(key, key => { key = this.getMetadataKeyRef(key); @@ -167,7 +174,12 @@ export abstract class CachedMapResource< return id; } - free(key: ResourceKey, id: string): void { + free(key: ResourceKey | typeof CachedResourceParamKey, id: string): void { + if (key === CachedResourceParamKey) { + super.free(CachedResourceParamKey, id); + return; + } + key = this.transformParam(key); key = this.getLockedKeys(key); @@ -177,7 +189,14 @@ export abstract class CachedMapResource< }); } - isOutdated(key: ResourceKey): boolean { + isOutdated(key?: ResourceKey): boolean { + if (key === undefined) { + return ( + Array.from(this.metadata.values()).some(metadata => metadata.outdated) + && this.loadedKeys.length === 0 + ); + } + if (this.isAlias(key) && !this.isAliasLoaded(key)) { return true; } @@ -195,7 +214,7 @@ export abstract class CachedMapResource< return ResourceKeyUtils.some(key, key => this.getMetadata(key).loading); } - markDataLoading(key: ResourceKey, includes?: string[]): void { + markDataLoading(key: ResourceKey, includes?: CachedResourceIncludeArgs): void { key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => { this.updateMetadata(key, metadata => { @@ -204,7 +223,7 @@ export abstract class CachedMapResource< }); } - markDataLoaded(key: ResourceKey, includes?: string[]): void { + markDataLoaded(key: ResourceKey, includes?: CachedResourceIncludeArgs): void { key = this.transformParam(key); if (includes) { @@ -285,8 +304,10 @@ export abstract class CachedMapResource< }); } - //@ts-expect-error fix - isLoaded(key: ResourceKey, includes?: CachedResourceIncludeArgs): boolean { + isLoaded( + key: ResourceKey, + includes?: CachedResourceIncludeArgs + ): boolean { if (this.isAlias(key) && !this.isAliasLoaded(key)) { return false; } @@ -300,7 +321,6 @@ export abstract class CachedMapResource< if (includes) { const metadata = this.getMetadata(key); - //@ts-expect-error fix if (includes.some(include => !metadata.includes.includes(include))) { return false; } @@ -355,51 +375,53 @@ export abstract class CachedMapResource< clear(): void { this.data.clear(); - this.metadata.clear(); + + const keys = Array.from(this.metadata.keys()) + .filter(key => key !== CachedResourceParamKey); + + for (const key of keys) { + this.metadata.delete(key); + } } - async refresh = []>( + async refresh>( key: TKey, includes?: T ): Promise>; - async refresh = []>( + async refresh>( key: ResourceKeyList, includes?: T ): Promise>>; - async refresh = []>( + async refresh>( key: ResourceKey, includes?: T ): Promise> | CachedResourceValueIncludes>; - async refresh = []>( + async refresh>( key: ResourceKey, includes?: T ): Promise> | CachedResourceValueIncludes> { - //@ts-expect-error fix await this.preLoadData(key, false, includes); - //@ts-expect-error fix await this.loadData(key, true, includes); return this.get(key) as Array> | CachedResourceValueIncludes; } - async load = []>( + async load>( key: TKey, includes?: T ): Promise>; - async load = []>( + async load>( key: ResourceKeyList, includes?: T ): Promise>>; - async load = []>( + async load>( key: ResourceKey, includes?: T ): Promise> | CachedResourceValueIncludes>; - async load = []>( + async load>( key: ResourceKey, includes?: T ): Promise> | CachedResourceValueIncludes> { - //@ts-expect-error fix await this.preLoadData(key, false, includes); - //@ts-expect-error fix await this.loadData(key, false, includes); return this.get(key) as Array> | CachedResourceValueIncludes; } @@ -429,7 +451,7 @@ export abstract class CachedMapResource< return ResourceKeyUtils.includes(param, key, this.isKeyEqual); } - getIncludes(key?: ResourceKey): string[] { + getIncludes(key?: ResourceKey): ReadonlyArray { if (key === undefined) { return this.defaultIncludes; } @@ -452,7 +474,10 @@ export abstract class CachedMapResource< * @param includes - Base includes * @returns {Object} Object where key is include name and value is true */ - getIncludesMap(key?: ResourceKey, includes: string[] = this.defaultIncludes): Record { + getIncludesMap( + key?: ResourceKey, + includes: ReadonlyArray = this.defaultIncludes + ): Record { const keyIncludes = this.getIncludes(key); return ['customIncludeBase', ...includes, ...keyIncludes].reduce((map, key) => { map[key] = true; @@ -557,7 +582,7 @@ export abstract class CachedMapResource< return this.data.has(key); } - protected commitIncludes(key: ResourceKey, includes: string[]): void { + protected commitIncludes(key: ResourceKey, includes: CachedResourceIncludeArgs): void { key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => { this.updateMetadata(key, metadata => { @@ -602,11 +627,11 @@ export abstract class CachedMapResource< export function getCachedMapResourceLoaderState< TKey, TValue, - TArguments = Record + TContext extends Record = Record >( - resource: CachedMapResource, + resource: CachedMapResource, key: ResourceKey, - includes?: CachedResourceIncludeArgs | undefined + includes?: CachedResourceIncludeArgs | undefined ): ILoadableState { return { get exception() { @@ -618,6 +643,9 @@ export function getCachedMapResourceLoaderState< isLoaded() { return resource.isLoaded(key, includes); }, + isError() { + return isContainsException(this.exception); + }, isOutdated() { return resource.isOutdated(key); }, diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index df06b0f481..65f3cfdc3c 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { observable, makeObservable, action } from 'mobx'; +import { observable, makeObservable, action, computed } from 'mobx'; import { Dependency } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor, IExecutorHandler, ISyncExecutor, SyncExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; @@ -35,27 +35,37 @@ export type IParamAlias = { getAlias: (param: TParam) => TParam; }; -export type CachedResourceData = TResource extends CachedResource ? T : never; -export type CachedResourceParam = TResource extends CachedResource ? T : never; -export type CachedResourceKey = TResource extends CachedResource ? T : never; -export type CachedResourceContext = TResource extends CachedResource ? T : void; +export type CachedResourceData = TResource extends CachedResource ? T : never; +export type CachedResourceValue = TResource extends CachedResource ? T : never; +export type CachedResourceParam = TResource extends CachedResource ? T : never; +export type CachedResourceKey = TResource extends CachedResource ? T : never; +export type CachedResourceContext = TResource extends CachedResource ? T : void; + +export const CachedResourceParamKey = Symbol('@cached-resource/param-default'); export abstract class CachedResource< TData, + TValue, TParam, - TKey = TParam, - TContext extends string[] | void = void + TKey, + TContext extends ReadonlyArray > extends Dependency { data: TData; - readonly onDataOutdated: ISyncExecutor; + get isResourceInUse(): boolean { + return Array.from(this.metadata.values()) + .some(metadata => metadata.dependencies.length > 0); + } + + readonly onUse: ISyncExecutor; + readonly onDataOutdated: ISyncExecutor; readonly onDataUpdate: ISyncExecutor; readonly onDataError: ISyncExecutor>; readonly beforeLoad: IExecutor; protected metadata: MetadataMap; protected loadedKeys: TParam[]; - protected defaultIncludes: string[]; + protected defaultIncludes: TContext; // needed for CachedResourceContext protected get loading(): boolean { return this.scheduler.executing; @@ -66,7 +76,12 @@ export abstract class CachedResource< protected logActivity: boolean; protected outdateWaitList: TParam[]; - constructor(defaultValue: TData, defaultIncludes: string[] = []) { + private readonly typescriptHack: TValue; + + constructor( + defaultValue: TData, + defaultIncludes: TContext = [] as any + ) { super(); this.isKeyEqual = this.isKeyEqual.bind(this); @@ -76,6 +91,7 @@ export abstract class CachedResource< this.logActivity = false; this.loadedKeys = []; + this.typescriptHack = null as any; this.defaultIncludes = defaultIncludes; this.paramAliases = []; this.outdateWaitList = []; @@ -89,10 +105,13 @@ export abstract class CachedResource< this.scheduler = new TaskScheduler(this.includes); this.data = defaultValue; this.beforeLoad = new Executor(null, this.includes); - this.onDataOutdated = new SyncExecutor(null); + this.onUse = new SyncExecutor(); + this.onDataOutdated = new SyncExecutor(null); this.onDataUpdate = new SyncExecutor(null); this.onDataError = new SyncExecutor>(null); + this.onUse.setInitialDataGetter(() => undefined); + if (this.logActivity) { // this.spy(this.beforeLoad, 'beforeLoad'); // this.spy(this.onDataOutdated, 'onDataOutdated'); @@ -101,11 +120,12 @@ export abstract class CachedResource< } makeObservable< - CachedResource, + this, 'loader' | 'loadedKeys' | 'commitIncludes' | 'resetIncludes' | 'markOutdatedSync' >(this, { loadedKeys: observable, data: observable, + isResourceInUse: computed, loader: action, markDataLoading: action, markDataLoaded: action, @@ -118,12 +138,46 @@ export abstract class CachedResource< use: action, free: action, }); + + setInterval(() => { + // mark resource outdate when it's not used + if (!this.isResourceInUse && !this.isOutdated()) { + this.markOutdated(); + + if (this.logActivity) { + console.log('Resource outdated lazy: ', this.getName()); + } + } + }, 5 * 60 * 1000); + } + + /** + * Mark resource as in use when {@link resource} is in use + * @param resource resource to depend on + */ + connect( + resource: CachedResource + ): void { + let subscription: string | null = null; + + resource.onUse.addHandler(() => { + if (resource.isResourceInUse) { + if (!subscription) { + subscription = this.use(CachedResourceParamKey); + } + } else { + if (subscription) { + this.free(CachedResourceParamKey, subscription); + subscription = null; + } + } + }); } sync( - resource: CachedResource, + resource: CachedResource, mapTo?: (param: TParam) => T, - mapOut?: (param: T) => TParam + mapOut?: (param: T | undefined) => TParam ): void { resource.outdateResource(this, mapOut); @@ -140,7 +194,7 @@ export abstract class CachedResource< this.preloadResource(resource, mapTo); } - updateResource(resource: CachedResource, map?: (param: TParam) => T): this { + updateResource(resource: CachedResource, map?: (param: TParam) => T): this { this.onDataUpdate.addHandler(param => { try { if (this.logActivity) { @@ -163,8 +217,8 @@ export abstract class CachedResource< } outdateResource( - resource: CachedResource, - map?: (param: TParam) => T + resource: CachedResource, + map?: (param: TParam | undefined) => T ): this { this.onDataOutdated.addHandler(param => { try { @@ -187,7 +241,9 @@ export abstract class CachedResource< return this; } - preloadResource(resource: CachedResource, map?: (param: TParam) => T): this { + preloadResource(resource: CachedResource, map?: (param: TParam) => T): this { + resource.connect(this); + this.beforeLoad.addHandler(async param => { try { if (this.logActivity) { @@ -198,7 +254,7 @@ export abstract class CachedResource< param = map(param) as any as TParam; } - await resource.load(param as any as T, undefined); + await resource.load(param as any as T, []); } finally { if (this.logActivity) { console.groupEnd(); @@ -231,22 +287,37 @@ export abstract class CachedResource< return this.getMetadata(param).dependencies.length > 0; } - use(param: TParam, id = uuid()): string { - this.updateMetadata(param, metadata => { + use(param: TParam | typeof CachedResourceParamKey, id = uuid()): string { + this.updateMetadata(param as TParam, metadata => { + if (param === CachedResourceParamKey) { + metadata.outdated = false; + } metadata.dependencies.push(id); }); + this.onUse.execute(); + if (this.logActivity) { + console.log('Use resource: ', this.getName(), param); + } return id; } - free(param: TParam, id: string): void { - this.updateMetadata(param, metadata => { + free(param: TParam | typeof CachedResourceParamKey, id: string): void { + this.updateMetadata(param as TParam, metadata => { if (metadata.dependencies.length > 0) { + if (param === CachedResourceParamKey) { + metadata.outdated = false; + } metadata.dependencies = metadata.dependencies.filter(v => v !== id); } }); + this.onUse.execute(); + + if (this.logActivity) { + console.log('Free resource: ', this.getName(), param); + } } - abstract isLoaded(param: TParam, context: TContext): boolean; + abstract isLoaded(param: TParam, context?: TContext): boolean; isAlias(key: TParam): boolean { return this.paramAliases.some(alias => { @@ -274,10 +345,7 @@ export abstract class CachedResource< key = this.transformParam(key); const metadata = this.getMetadata(key); - if (includes) { - return (includes as string[]).every(include => metadata.includes.includes(include)); - } - return true; + return includes.every(include => metadata.includes.includes(include)); } getMetadata(param: TParam): ICachedResourceMetadata { @@ -299,7 +367,14 @@ export abstract class CachedResource< return this.getMetadata(param).exception; } - isOutdated(param: TParam): boolean { + isOutdated(param?: TParam): boolean { + if (param === undefined) { + return ( + Array.from(this.metadata.values()).some(metadata => metadata.outdated) + && this.loadedKeys.length === 0 + ); + } + if (this.isAlias(param) && !this.isAliasLoaded(param)) { return true; } @@ -314,18 +389,18 @@ export abstract class CachedResource< return this.getMetadata(param).loading; } - markDataLoading(param: TParam, context: TContext): void { + markDataLoading(param: TParam, context?: TContext): void { param = this.transformParam(param); this.updateMetadata(param, metadata => { metadata.loading = true; }); } - markDataLoaded(param: TParam, includes: TContext): void { + markDataLoaded(param: TParam, includes?: TContext): void { param = this.transformParam(param); if (includes) { - this.commitIncludes(param, includes as string[]); + this.commitIncludes(param, includes); } this.updateMetadata(param, metadata => { @@ -333,7 +408,7 @@ export abstract class CachedResource< }); } - markDataError(exception: Error, param: TParam, context: TContext): void { + markDataError(exception: Error, param: TParam, context?: TContext): void { if (this.isAlias(param) && !this.isAliasLoaded(param)) { this.loadedKeys.push(param); } @@ -353,13 +428,15 @@ export abstract class CachedResource< }); } - markOutdated(param: TParam): void { - if (this.scheduler.isExecuting(param) && !this.outdateWaitList.some(key => this.includes(param, key))) { - this.outdateWaitList.push(param); + markOutdated(param?: TParam): void { + const isKeyExecuting = param === undefined ? this.scheduler.executing : this.scheduler.isExecuting(param); + + if (isKeyExecuting && !this.outdateWaitList.some(key => this.includes(param!, key))) { + this.outdateWaitList.push(param!); return; } - this.markOutdatedSync(param); + this.markOutdatedSync(param!); } markUpdated(param: TParam): void { @@ -426,19 +503,19 @@ export abstract class CachedResource< return param; } - async refresh(param: TParam, context: TContext): Promise { + async refresh(param: TParam, context?: TContext): Promise { await this.preLoadData(param, false, context); await this.loadData(param, true, context); return this.data; } - async load(param: TParam, context: TContext): Promise { + async load(param: TParam, context?: TContext): Promise { await this.preLoadData(param, false, context); await this.loadData(param, false, context); return this.data; } - getIncludes(key?: TParam): string[] { + getIncludes(key?: TParam): ReadonlyArray { if (key === undefined) { return this.defaultIncludes; } @@ -449,7 +526,10 @@ export abstract class CachedResource< return metadata.includes; } - getIncludesMap(key?: TParam, includes: string[] = this.defaultIncludes): Record { + getIncludesMap( + key?: TParam, + includes: ReadonlyArray = this.defaultIncludes + ): Record { const keyIncludes = this.getIncludes(key); return ['customIncludeBase', ...includes, ...keyIncludes].reduce((map, key) => { map[key] = true; @@ -464,7 +544,7 @@ export abstract class CachedResource< } } - protected commitIncludes(key: TParam, includes: string[]): void { + protected commitIncludes(key: TParam, includes: ReadonlyArray): void { key = this.transformParam(key); this.updateMetadata(key, metadata => { for (const include of includes) { @@ -480,19 +560,35 @@ export abstract class CachedResource< this.data = data; } - protected markOutdatedSync(param: TParam): void { - if (this.isAlias(param)) { - const index = this.loadedKeys.findIndex(key => this.isAliasEqual(param, key)); + protected markOutdatedSync(param?: TParam): void { + if (param === undefined) { + for (const param of this.metadata.keys()) { + this.updateMetadata(param as unknown as TParam, metadata => { + if (param === CachedResourceParamKey) { + return; + } - if (index >= 0) { - this.loadedKeys.splice(index, 1); + metadata.outdated = true; + }); } + this.loadedKeys = []; + this.resetIncludes(); + } else { + + if (this.isAlias(param)) { + const index = this.loadedKeys.findIndex(key => this.isAliasEqual(param!, key)); + + if (index >= 0) { + this.loadedKeys.splice(index, 1); + } + } + + param = this.transformParam(param); + this.updateMetadata(param, metadata => { + metadata.outdated = true; + }); } - param = this.transformParam(param); - this.updateMetadata(param, metadata => { - metadata.outdated = true; - }); this.onDataOutdated.execute(param); } @@ -514,7 +610,7 @@ export abstract class CachedResource< return param === second; } - protected includes(param: TParam, second: TParam): boolean { + includes(param: TParam, second: TParam): boolean { if (this.isAliasEqual(param, second)) { return true; } @@ -524,25 +620,29 @@ export abstract class CachedResource< return this.isKeyEqual(param, second); } - protected abstract loader(param: TParam, context: TContext, refresh: boolean): Promise; + protected abstract loader( + param: TParam, + context: ReadonlyArray | undefined, + refresh: boolean + ): Promise; protected async performUpdate( param: TParam, - context: TContext, - update: (param: TParam, context: TContext) => Promise, + context: TContext | undefined, + update: (param: TParam, context?: ReadonlyArray) => Promise, ): Promise; protected async performUpdate( param: TParam, - context: TContext, - update: (param: TParam, context: TContext) => Promise, - exitCheck: (param: TParam, context: TContext) => boolean + context: TContext | undefined, + update: (param: TParam, context?: ReadonlyArray) => Promise, + exitCheck: (param: TParam, context?: ReadonlyArray) => boolean ): Promise; protected async performUpdate( param: TParam, - context: TContext, - update: (param: TParam, context: TContext) => Promise, - exitCheck?: (param: TParam, context: TContext) => boolean + context: TContext | undefined, + update: (param: TParam, context?: ReadonlyArray) => Promise, + exitCheck?: (param: TParam, context?: ReadonlyArray) => boolean ): Promise { const contexts = await this.beforeLoad.execute(param); @@ -587,9 +687,17 @@ export abstract class CachedResource< }); } - protected async preLoadData(param: TParam, refresh: boolean, context: TContext): Promise { } + protected async preLoadData( + param: TParam, + refresh: boolean, + context?: TContext + ): Promise { } - protected async loadData(param: TParam, refresh: boolean, context: TContext): Promise { + protected async loadData( + param: TParam, + refresh: boolean, + context?: TContext + ): Promise { if (!refresh) { await this.scheduler.waitRelease(param); @@ -637,15 +745,23 @@ export abstract class CachedResource< }); } - private async loadingTask(param: TParam, context: TContext, refresh: boolean) { + private async loadingTask( + param: TParam, + context: ReadonlyArray | undefined, + refresh: boolean + ) { this.setData(await this.loader(param, context, refresh)); } private async taskWrapper( param: TParam, - context: TContext, + context: ReadonlyArray | undefined, refresh: boolean, - promise: (param: TParam, context: TContext, refresh: boolean) => Promise + promise: ( + param: TParam, + context: ReadonlyArray | undefined, + refresh: boolean + ) => Promise ) { if (this.logActivity) { console.log(this.getActionPrefixedName('loading')); @@ -679,7 +795,7 @@ export abstract class CachedResource< }; protected getActionPrefixedName(action: string): string { - return this.constructor.name + ': ' + action; + return this.getName() + ': ' + action; } private readonly logInterrupted = (action: string): IExecutorHandler => (data, contexts) => { diff --git a/webapp/packages/core-sdk/src/Resource/CachedResourceIncludes.ts b/webapp/packages/core-sdk/src/Resource/CachedResourceIncludes.ts index db58410991..6ef859aac9 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResourceIncludes.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResourceIncludes.ts @@ -18,12 +18,17 @@ export type CachedResourceIncludeFlags = { }; export type CachedResourceIncludeList = Array>; -export type CachedResourceIncludeToKey = TKey extends Array<`include${infer T}` | `customInclude${Capitalize}`> ? Uncapitalize : unknown; -export type CachedResourceIncludeArgs = Array< -keyof CachedResourceIncludeFlags< -Exclude, undefined | null>, -TArguments> ->; +export type CachedResourceIncludeToKey = TKey extends ReadonlyArray<`include${infer T}` | `customInclude${Capitalize}`> ? Uncapitalize : unknown; +export type CachedResourceIncludeArgs = TArguments extends Record + ? string[] + : ( + Array< + Exclude< + keyof CachedResourceIncludeFlags, undefined | null>, TArguments>, + number | symbol + > + > + ); export type ApplyIncludes = TValue & ({ diff --git a/webapp/packages/core-sdk/src/Resource/EventSource.ts b/webapp/packages/core-sdk/src/Resource/EventSource.ts deleted file mode 100644 index 5f35806395..0000000000 --- a/webapp/packages/core-sdk/src/Resource/EventSource.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2022 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 { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; - -import type { CachedResource } from './CachedResource'; - -export type EventSourceCallback = (event: T) => any; -export interface IEventSource { - on( - resource: EventSourceCallback, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this; -} - -const DEFAULT_INTERVAL = 1000; - -export abstract class EventSource implements IEventSource { - onEvent: ISyncExecutor; - - private listening: boolean; - private readonly interval: number; - constructor(interval?: number) { - this.onEvent = new SyncExecutor(); - this.interval = interval ?? DEFAULT_INTERVAL; - this.listening = false; - } - - on( - resource: EventSourceCallback, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this; - on( - resource: CachedResource, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this; - on( - resource: EventSourceCallback | CachedResource, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this { - if (typeof resource === 'function') { - this.onEvent.addHandler(event => { - if (!filter || filter(event)) { - resource(mapTo(event)); - } - }); - } else { - this.onEvent.addHandler(event => { - if (!filter || filter(event)) { - resource.markOutdated(mapTo(event)); - } - }); - } - - if (!this.listening) { - this.listen(); - } - - return this; - } - - listen() { - if (this.listening) { - console.warn('Already listening events'); - return; - } - - this.listening = true; - let failedRequests = 0; - let interval = this.interval; - - const iteration = async () => { - try { - await this.listener(); - failedRequests = 0; - } catch (exception: any) { - console.error(exception); - failedRequests++; - } - - interval = this.interval * Math.min((failedRequests || 1), 10); - - if (this.listening) { - setTimeout(iteration, interval); - } - }; - - iteration(); - } - - stop() { - this.listening = false; - } - - event(event: TEvent) { - this.onEvent.execute(event); - } - - protected abstract listener(): Promise; -} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts b/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts deleted file mode 100644 index 9cc11ecb66..0000000000 --- a/webapp/packages/core-sdk/src/Resource/ResourceEventHandler.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2022 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 { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; - -import type { CachedResource } from './CachedResource'; -import type { IEventSource, EventSourceCallback } from './EventSource'; - -export abstract class ResourceEventHandler implements IEventSource { - onEvent: ISyncExecutor; - - private subscribed: boolean; - constructor(private readonly source?: IEventSource) { - this.onEvent = new SyncExecutor(); - this.subscribed = false; - } - - on( - callback: EventSourceCallback, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this; - on( - resource: CachedResource, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this; - on( - resource: CachedResource | EventSourceCallback, - mapTo: (param: TEvent) => T, - filter?: (param: TEvent) => boolean, - ): this { - if (typeof resource === 'function') { - this.onEvent.addHandler(event => { - if (!filter || filter(event)) { - resource(mapTo(event)); - } - }); - } else { - this.onEvent.addHandler(event => { - if (!filter || filter(event)) { - resource.markOutdated(mapTo(event)); - } - }); - } - - if (!this.subscribed) { - this.subscribed = true; - this.source?.on(this.event.bind(this), this.map.bind(this), this.filter.bind(this)); - } - - return this; - } - - event(event: TEvent) { - this.onEvent.execute(event); - } - - abstract map(event: SourceEvent): TEvent; - abstract filter(event: SourceEvent): boolean; -} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts index ca23605a08..0bd9b6fa52 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts @@ -7,6 +7,7 @@ */ export type ResourceKey = TKey | ResourceKeyList; +export type ResourceKeyType = TKey extends ResourceKeyList ? I : TKey; export type SingleResourceKey = Exclude>> extends ResourceKeyList ? TKey : T; diff --git a/webapp/packages/core-sdk/src/ServiceError.ts b/webapp/packages/core-sdk/src/ServiceError.ts new file mode 100644 index 0000000000..7f39045c52 --- /dev/null +++ b/webapp/packages/core-sdk/src/ServiceError.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { DetailsError } from './DetailsError'; + +export class ServiceError extends DetailsError { + constructor(message?: string) { + super(message); + this.name = 'Service Error'; + this.errorMessage = message || 'Error'; + } + + hasDetails(): boolean { + return false; + } +} diff --git a/webapp/packages/core-sdk/src/index.ts b/webapp/packages/core-sdk/src/index.ts index bfbb07a705..72578559b6 100644 --- a/webapp/packages/core-sdk/src/index.ts +++ b/webapp/packages/core-sdk/src/index.ts @@ -5,8 +5,6 @@ export * from './Resource/CachedMapResource'; export * from './Resource/CachedResource'; export * from './Resource/CachedResourceIncludes'; export * from './Resource/ResourceKeyList'; -export * from './Resource/EventSource'; -export * from './Resource/ResourceEventHandler'; export * from './CustomGraphQLClient'; export * from './DetailsError'; export * from './EnvironmentService'; @@ -19,4 +17,5 @@ export * from './GraphQLService'; export * from './isObjectPropertyInfoStateEqual'; export * from './sdk'; export * from './ServerInternalError'; +export * from './ServiceError'; export { manifest as coreSDKManifest } from './manifest'; diff --git a/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql b/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql deleted file mode 100644 index d4a04d5844..0000000000 --- a/webapp/packages/core-sdk/src/queries/session/getSessionEvents.gql +++ /dev/null @@ -1,10 +0,0 @@ -query getSessionEvents( - $maxEntries: Int! -) { - events: readSessionEvents( - maxEntries: $maxEntries - ) { - eventType - eventData - } -} diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 9bf0c03ed1..9245ebe4b1 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -165,29 +165,80 @@ export enum AuthStatus { Success = 'SUCCESS' } -export interface CbEvent { - eventData: Scalars['Object']; - eventType: CbEventType; +export type CbClientEvent = { + id: CbClientEventId; + topicId?: Maybe; +}; + +export enum CbClientEventId { + CbClientProjectsActive = 'cb_client_projects_active', + CbClientTopicSubscribe = 'cb_client_topic_subscribe', + CbClientTopicUnsubscribe = 'cb_client_topic_unsubscribe' } -export enum CbEventResourceType { - Datasource = 'DATASOURCE', - RmResource = 'RM_RESOURCE' +export interface CbConfigEvent extends CbServerEvent { + id: CbServerEventId; + topicId?: Maybe; } -export enum CbEventStatus { - TypeCreate = 'TYPE_CREATE', - TypeDelete = 'TYPE_DELETE', - TypeUpdate = 'TYPE_UPDATE' +export interface CbDatasourceEvent extends CbServerEvent { + dataSourceIds: Array; + id: CbServerEventId; + projectId: Scalars['String']; + topicId?: Maybe; } -export enum CbEventType { +export interface CbDatasourceFolderEvent extends CbServerEvent { + id: CbServerEventId; + nodePaths: Array; + projectId: Scalars['String']; + topicId?: Maybe; +} + +export enum CbEventTopic { + CbConfig = 'cb_config', + CbDatasource = 'cb_datasource', + CbDatasourceFolder = 'cb_datasource_folder', + CbProjects = 'cb_projects', + CbScripts = 'cb_scripts' +} + +export interface CbProjectsActiveEvent extends CbClientEvent { + id: CbClientEventId; + projects: Array; + topicId?: Maybe; +} + +export interface CbrmEvent extends CbServerEvent { + id: CbServerEventId; + projectId: Scalars['String']; + resourcePath: Scalars['String']; + topicId?: Maybe; +} + +export type CbServerEvent = { + id: CbServerEventId; + topicId?: Maybe; +}; + +export enum CbServerEventId { CbConfigChanged = 'cb_config_changed', + CbDatasourceCreated = 'cb_datasource_created', + CbDatasourceDeleted = 'cb_datasource_deleted', + CbDatasourceFolderCreated = 'cb_datasource_folder_created', + CbDatasourceFolderDeleted = 'cb_datasource_folder_deleted', CbDatasourceFolderUpdated = 'cb_datasource_folder_updated', CbDatasourceUpdated = 'cb_datasource_updated', + CbRmResourceCreated = 'cb_rm_resource_created', + CbRmResourceDeleted = 'cb_rm_resource_deleted', CbRmResourceUpdated = 'cb_rm_resource_updated' } +export interface CbTopicEvent extends CbClientEvent { + id: CbClientEventId; + topicId: CbEventTopic; +} + export interface ConnectionConfig { authModelId?: InputMaybe; configurationType?: InputMaybe; @@ -419,6 +470,7 @@ export interface Mutation { createConnectionFromTemplate: ConnectionInfo; deleteConnection: Scalars['Boolean']; deleteConnectionFolder: Scalars['Boolean']; + emptyEventMutation?: Maybe; initConnection: ConnectionInfo; navDeleteNodes?: Maybe; navMoveNodesToFolder?: Maybe; @@ -925,6 +977,7 @@ export interface Query { deleteUser?: Maybe; deleteUserMetaParameter: Scalars['Boolean']; driverList: Array; + emptyEvent?: Maybe; enableUser?: Maybe; getConnectionSubjectAccess: Array; getSubjectConnectionAccess: Array; @@ -946,7 +999,6 @@ export interface Query { navNodeParents: Array; navRefreshNode?: Maybe; networkHandlers: Array; - readSessionEvents: Array; readSessionLog: Array; revokeUserTeam?: Maybe; rmListProjectGrantedPermissions: Array; @@ -1170,11 +1222,6 @@ export interface QueryNavRefreshNodeArgs { } -export interface QueryReadSessionEventsArgs { - maxEntries: Scalars['Int']; -} - - export interface QueryReadSessionLogArgs { clearEntries?: InputMaybe; maxEntries?: InputMaybe; @@ -2712,13 +2759,6 @@ export type ChangeSessionLanguageMutationVariables = Exact<{ export type ChangeSessionLanguageMutation = { changeSessionLanguage?: boolean }; -export type GetSessionEventsQueryVariables = Exact<{ - maxEntries: Scalars['Int']; -}>; - - -export type GetSessionEventsQuery = { events: Array<{ eventType: CbEventType, eventData: any }> }; - export type OpenSessionMutationVariables = Exact<{ defaultLocale?: InputMaybe; }>; @@ -4318,14 +4358,6 @@ export const ChangeSessionLanguageDocument = ` changeSessionLanguage(locale: $locale) } `; -export const GetSessionEventsDocument = ` - query getSessionEvents($maxEntries: Int!) { - events: readSessionEvents(maxEntries: $maxEntries) { - eventType - eventData - } -} - `; export const OpenSessionDocument = ` mutation openSession($defaultLocale: String) { session: openSession(defaultLocale: $defaultLocale) { @@ -4848,9 +4880,6 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = changeSessionLanguage(variables: ChangeSessionLanguageMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(ChangeSessionLanguageDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'changeSessionLanguage', 'mutation'); }, - getSessionEvents(variables: GetSessionEventsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetSessionEventsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSessionEvents', 'query'); - }, openSession(variables?: OpenSessionMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(OpenSessionDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'openSession', 'mutation'); }, diff --git a/webapp/packages/core-utils/src/GlobalConstants.ts b/webapp/packages/core-utils/src/GlobalConstants.ts index 8bb94743f8..1c4fb7c93e 100644 --- a/webapp/packages/core-utils/src/GlobalConstants.ts +++ b/webapp/packages/core-utils/src/GlobalConstants.ts @@ -21,6 +21,18 @@ export const GlobalConstants = { return _VERSION_; }, + get protocol(): 'http:' | 'https:' { + return window.location.protocol as 'http:' | 'https:'; + }, + + get wsProtocol(): 'ws:' | 'wss:' { + return this.protocol === 'https:' ? 'wss:' : 'ws:'; + }, + + get host(): string { + return window.location.host; + }, + get rootURI(): string { const defaultURI = '/'; @@ -42,6 +54,10 @@ export const GlobalConstants = { return pathJoin(this.serviceURI, ...parts); }, + hostServiceUrl(...parts: string[]): string { + return pathJoin(this.host, this.absoluteServiceUrl(...parts)); + }, + absoluteUrl(...parts: string[]): string { if (parts[0].startsWith('platform:')) { return this.absoluteServiceUrl('images', ...parts); diff --git a/webapp/packages/core-utils/src/ILoadableState.ts b/webapp/packages/core-utils/src/ILoadableState.ts index b41f485241..e682b8099f 100644 --- a/webapp/packages/core-utils/src/ILoadableState.ts +++ b/webapp/packages/core-utils/src/ILoadableState.ts @@ -9,6 +9,7 @@ export interface ILoadableState { isLoading: () => boolean; isLoaded: () => boolean; + isError: () => boolean; readonly exception?: (Error | null)[] | Error | null; load: () => void; reload?: () => void; diff --git a/webapp/packages/core-utils/src/underscore.ts b/webapp/packages/core-utils/src/underscore.ts index 37575bd5d4..7d66cfb00d 100644 --- a/webapp/packages/core-utils/src/underscore.ts +++ b/webapp/packages/core-utils/src/underscore.ts @@ -6,4 +6,4 @@ * you may not use this file except in compliance with the License. */ -export { uniq, groupBy, iteratee, values as objectValues } from 'underscore'; \ No newline at end of file +export { uniq, groupBy, iteratee, values as objectValues, compose } from 'underscore'; \ No newline at end of file diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationFeaturesForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationFeaturesForm.tsx index 82d7c81360..139c1ba5e2 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationFeaturesForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationFeaturesForm.tsx @@ -11,7 +11,7 @@ import { useContext } from 'react'; import styled from 'reshadow'; import { AdministrationSettingsService } from '@cloudbeaver/core-administration'; -import { BASE_CONTAINERS_STYLES, FormContext, GroupTitle, Loader, PlaceholderComponent, Switch, useDataResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, FormContext, GroupTitle, Loader, PlaceholderComponent, Switch, useResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { FeaturesResource } from '@cloudbeaver/core-root'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; @@ -22,7 +22,7 @@ export const ServerConfigurationFeaturesForm: PlaceholderComponent(function ServerCon serverConfig, }) { const translate = useTranslate(); - const driversResource = useMapResource(ServerConfigurationDriversForm, DBDriverResource, CachedMapAllKey); + const driversResource = useResource(ServerConfigurationDriversForm, DBDriverResource, CachedMapAllKey); const drivers = driversResource.resource.values.slice().sort(driversResource.resource.compare); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx index 2331c6255e..6179972148 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/AuthConfigurationsTable/AuthConfiguration.tsx @@ -12,7 +12,7 @@ import styled, { css, use } from 'reshadow'; import { AuthProvidersResource } from '@cloudbeaver/core-authentication'; import { TableItem, TableColumnValue, TableItemSelect, TableItemExpand, - Placeholder, StaticImage, useMapResource, FieldCheckbox + Placeholder, StaticImage, useResource, FieldCheckbox } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { AdminAuthProviderConfiguration } from '@cloudbeaver/core-sdk'; @@ -43,7 +43,7 @@ interface Props { export const AuthConfiguration = observer(function AuthConfiguration({ configuration }) { const service = useService(AuthConfigurationsAdministrationService); - const resource = useMapResource(AuthConfiguration, AuthProvidersResource, configuration.providerId); + const resource = useResource(AuthConfiguration, AuthProvidersResource, configuration.providerId); const icon = configuration.iconURL || resource.data?.icon; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx index c7ff75c57c..36d034217f 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/IdentityProviders/Options/AuthConfigurationOptions.tsx @@ -14,7 +14,7 @@ import { AuthConfigurationParametersResource, AuthProvidersResource } from '@clo import { BASE_CONTAINERS_STYLES, ColoredContainer, Combobox, FieldCheckbox, Group, GroupTitle, InputField, Link, ObjectPropertyInfoForm, SubmittingForm, - Textarea, useClipboard, useMapResource, useObjectPropertyCategories, useStyles, useTranslate + Textarea, useClipboard, useResource, useObjectPropertyCategories, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { AuthProviderConfigurationParametersFragment, CachedMapAllKey } from '@cloudbeaver/core-sdk'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; @@ -39,8 +39,8 @@ export const AuthConfigurationOptions: TabContainerPanelComponent(function MetaParameterInfo className, style, }) { - const userMetaParameters = useDataResource(MetaParameterInfoPanel, UserMetaParametersResource, undefined); + const userMetaParameters = useResource(MetaParameterInfoPanel, UserMetaParametersResource, undefined); const styles = useStyles(style, BASE_CONTAINERS_STYLES); const translate = useTranslate(); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParameters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParameters.tsx index b15139e281..1a3ff45022 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParameters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/MetaParameters/MetaParameters.tsx @@ -11,7 +11,7 @@ import styled, { css, use } from 'reshadow'; import type { IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; import { UserMetaParametersResource } from '@cloudbeaver/core-authentication'; -import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect, useDataResource, ToolsPanel, ToolsAction, Loader, BASE_LAYOUT_GRID_STYLES, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect, useResource, ToolsPanel, ToolsAction, Loader, BASE_LAYOUT_GRID_STYLES, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { useController, useService } from '@cloudbeaver/core-di'; @@ -53,7 +53,7 @@ export const MetaParameters = observer(function MetaParameters({ sub, par const translate = useTranslate(); const controller = useController(MetaParametersController); const createMetaParameterService = useService(CreateMetaParameterService); - const userMetaParametersResource = useDataResource(MetaParameters, UserMetaParametersResource, undefined); + const userMetaParametersResource = useResource(MetaParameters, UserMetaParametersResource, undefined); const keys = userMetaParametersResource.data.map(metadata => metadata.id); const isLocalProviderAvailable = controller.isLocalProviderAvailable; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx index 1cdd2727ae..e08c03c413 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedConnections/GrantedConnections.tsx @@ -12,7 +12,7 @@ import styled, { css } from 'reshadow'; import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, getComputed, Group, - InfoItem, Loader, TextPlaceholder, useMapResource, useStyles, useTranslate + InfoItem, Loader, TextPlaceholder, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, DBDriverResource, isCloudConnection } from '@cloudbeaver/core-connections'; import type { TLocalizationToken } from '@cloudbeaver/core-localization'; @@ -52,16 +52,16 @@ export const GrantedConnections: TabContainerPanelComponent = ob const { selected } = useTab(tabId); const loaded = state.state.loaded; - const projects = useMapResource(GrantedConnections, ProjectInfoResource, CachedMapAllKey); + const projects = useResource(GrantedConnections, ProjectInfoResource, CachedMapAllKey); - const dbDriverResource = useMapResource( + const dbDriverResource = useResource( GrantedConnections, DBDriverResource, CachedMapAllKey, { active: selected } ); - const connections = useMapResource( + const connections = useResource( GrantedConnections, ConnectionInfoResource, CachedMapAllKey, diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx index 2f50629d49..69f9dca4d8 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/GrantedUsers/GrantedUsers.tsx @@ -14,7 +14,7 @@ import styled, { css } from 'reshadow'; import { UsersResource } from '@cloudbeaver/core-authentication'; import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, - InfoItem, Loader, TextPlaceholder, useMapResource, useStyles, useTranslate + InfoItem, Loader, TextPlaceholder, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; @@ -51,7 +51,7 @@ export const GrantedUsers: TabContainerPanelComponent = observer const state = useGrantedUsers(formState.config, formState.mode); const { selected } = useTab(tabId); - const users = useMapResource(GrantedUsers, UsersResource, selected ? CachedMapAllKey : null); + const users = useResource(GrantedUsers, UsersResource, selected ? CachedMapAllKey : null); const grantedUsers = useMemo(() => computed(() => users.resource.values .filter(user => state.state.grantedUsers.includes(user.userId)) diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx index 2f17cbe570..8d5bd4c695 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/Permissions.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { PermissionsResource } from '@cloudbeaver/core-administration'; -import { BASE_CONTAINERS_STYLES, FieldCheckbox, Group, GroupTitle, useMapResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, FieldCheckbox, Group, GroupTitle, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import type { ITeamFormProps } from '../ITeamFormProps'; @@ -23,7 +23,7 @@ const styles = css` export const Permissions = observer(function Permissions({ state }) { const translate = useTranslate(); - const permissionsResource = useMapResource(Permissions, PermissionsResource, CachedMapAllKey); + const permissionsResource = useResource(Permissions, PermissionsResource, CachedMapAllKey); const style = useStyles(BASE_CONTAINERS_STYLES, styles); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx index 419fa8a9f8..1ca78fc402 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/Options/TeamOptions.tsx @@ -11,7 +11,7 @@ import { useRef } from 'react'; import styled, { css } from 'reshadow'; import { TeamMetaParametersResource } from '@cloudbeaver/core-authentication'; -import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, InputField, SubmittingForm, Textarea, useTranslate, useStyles, useDataResource, Loader, GroupTitle, ObjectPropertyInfoForm } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, InputField, SubmittingForm, Textarea, useTranslate, useStyles, useResource, Loader, GroupTitle, ObjectPropertyInfoForm } from '@cloudbeaver/core-blocks'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; @@ -31,8 +31,8 @@ export const TeamOptions: TabContainerPanelComponent = observer( const formRef = useRef(null); const translate = useTranslate(); - const serverConfigResource = useDataResource(TeamOptions, ServerConfigResource, undefined); - const teamMetaParameters = useDataResource(TeamOptions, TeamMetaParametersResource, undefined); + const serverConfigResource = useResource(TeamOptions, ServerConfigResource, undefined); + const teamMetaParameters = useResource(TeamOptions, TeamMetaParametersResource, undefined); const style = useStyles(BASE_CONTAINERS_STYLES, styles); const edit = state.mode === 'edit'; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx index 90f7bac601..5069f07733 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/useTeamsTable.tsx @@ -9,7 +9,7 @@ import { computed, observable } from 'mobx'; import { compareTeams, TeamInfo, TeamsResource } from '@cloudbeaver/core-authentication'; -import { TableState, useMapResource, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; +import { TableState, useResource, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -28,7 +28,7 @@ interface State { export function useTeamsTable(): Readonly { const notificationService = useService(NotificationService); const dialogService = useService(CommonDialogService); - const resource = useMapResource(useTeamsTable, TeamsResource, CachedMapAllKey); + const resource = useResource(useTeamsTable, TeamsResource, CachedMapAllKey); const translate = useTranslate(); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx index 68b2af96bf..a119a740e9 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx @@ -11,7 +11,7 @@ import { useCallback } from 'react'; import styled, { css } from 'reshadow'; import { AuthRolesResource, UserMetaParametersResource } from '@cloudbeaver/core-authentication'; -import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, FieldCheckbox, Group, GroupTitle, InputField, Loader, ObjectPropertyInfoForm, useDataResource, useTranslate, useStyles, Combobox } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, FieldCheckbox, Group, GroupTitle, InputField, Loader, ObjectPropertyInfoForm, useResource, useTranslate, useStyles, Combobox } from '@cloudbeaver/core-blocks'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import type { IUserFormProps } from './UserFormService'; @@ -28,8 +28,8 @@ export const UserInfo: TabContainerPanelComponent = observer(fun }) { const style = useStyles(BASE_CONTAINERS_STYLES, styles); const translate = useTranslate(); - const userMetaParameters = useDataResource(UserInfo, UserMetaParametersResource, undefined); - const authRoles = useDataResource(UserInfo, AuthRolesResource, undefined); + const userMetaParameters = useResource(UserInfo, UserMetaParametersResource, undefined); + const authRoles = useResource(UserInfo, AuthRolesResource, undefined); const handleTeamChange = useCallback( (teamId: string, value: boolean) => { controller.credentials.teams.set(teamId, value); }, diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx index 1ccb4e4b84..4c343b92d6 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx @@ -11,7 +11,7 @@ import { useState } from 'react'; import styled, { css, use } from 'reshadow'; import { AuthRolesResource } from '@cloudbeaver/core-authentication'; -import { BASE_CONTAINERS_STYLES, Combobox, Filter, Group, IconOrImage, useDataResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, Combobox, Filter, Group, IconOrImage, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { IUserFilters, USER_ROLE_ALL, USER_STATUSES } from './useUsersTableFilters'; @@ -63,7 +63,7 @@ interface Props { export const UsersTableFilters = observer(function UsersTableFilters({ filters }) { const translate = useTranslate(); const style = useStyles(BASE_CONTAINERS_STYLES, styles); - const authRolesResource = useDataResource(UsersTableFilters, AuthRolesResource, undefined); + const authRolesResource = useResource(UsersTableFilters, AuthRolesResource, undefined); const [open, setOpen] = useState(false); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx index e6d65de2ab..236d3eca76 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx @@ -12,8 +12,8 @@ import styled, { css } from 'reshadow'; import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; import { AuthProvidersResource, AuthRolesResource, AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication'; import { - Table, TableHeader, TableColumnHeader, TableBody, useMapResource, ToolsAction, - ToolsPanel, Loader, useTranslate, useStyles, useDataResource, + Table, TableHeader, TableColumnHeader, TableBody, useResource, ToolsAction, + ToolsPanel, Loader, useTranslate, useStyles, BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; @@ -77,9 +77,9 @@ export const UsersTable = observer(function UsersTable({ sub, param }) { const createUserService = useService(CreateUserService); - const usersResource = useMapResource(UsersTable, UsersResource, CachedMapAllKey); - const authProvidersResource = useMapResource(UsersTable, AuthProvidersResource, CachedMapAllKey); - const authRolesResource = useDataResource(UsersTable, AuthRolesResource, undefined); + const usersResource = useResource(UsersTable, UsersResource, CachedMapAllKey); + const authProvidersResource = useResource(UsersTable, AuthProvidersResource, CachedMapAllKey); + const authRolesResource = useResource(UsersTable, AuthRolesResource, undefined); const table = useUsersTable(usersResource.resource); const filters = useUsersTableFilters(table.users); diff --git a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts index fafb40d24e..ebde9bdc80 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/useAuthDialogState.ts @@ -11,7 +11,7 @@ import { useEffect } from 'react'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AuthInfoService, AuthProvider, AuthProviderConfiguration, AuthProvidersResource, IAuthCredentials } from '@cloudbeaver/core-authentication'; -import { useMapResource, useObservableRef } from '@cloudbeaver/core-blocks'; +import { useResource, useObservableRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; @@ -45,7 +45,7 @@ interface IState { } export function useAuthDialogState(accessRequest: boolean, providerId: string | null, configurationId?: string): IData { - const authProvidersResource = useMapResource(useAuthDialogState, AuthProvidersResource, CachedMapAllKey); + const authProvidersResource = useResource(useAuthDialogState, AuthProvidersResource, CachedMapAllKey); const administrationScreenService = useService(AdministrationScreenService); const authInfoService = useService(AuthInfoService); const notificationService = useService(NotificationService); diff --git a/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx index d082b84463..842529f15b 100644 --- a/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx +++ b/webapp/packages/plugin-connection-search/src/Search/SearchDatabase.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; -import { useMapResource } from '@cloudbeaver/core-blocks'; +import { useResource } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; @@ -21,8 +21,8 @@ import { DatabaseList } from './DatabaseList'; export const SearchDatabase: React.FC = observer(function SearchDatabase() { const connectionSearchService = useService(ConnectionSearchService); - useMapResource(SearchDatabase, ProjectInfoResource, CachedMapAllKey); - useMapResource(SearchDatabase, DBDriverResource, CachedMapAllKey); + useResource(SearchDatabase, ProjectInfoResource, CachedMapAllKey); + useResource(SearchDatabase, DBDriverResource, CachedMapAllKey); function select(database: AdminConnectionSearchInfo) { connectionSearchService.select(database); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx index 5b250c910d..f902aa13f2 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsAdministration.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { AdministrationItemContentProps, ADMINISTRATION_TOOLS_PANEL_STYLES } from '@cloudbeaver/core-administration'; -import { Loader, useMapResource, ToolsAction, ToolsPanel, BASE_LAYOUT_GRID_STYLES, useTranslate, useStyles, Translate } from '@cloudbeaver/core-blocks'; +import { Loader, useResource, ToolsAction, ToolsPanel, BASE_LAYOUT_GRID_STYLES, useTranslate, useStyles, Translate } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useController, useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; @@ -61,7 +61,7 @@ export const ConnectionsAdministration = observer diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx index 812aaef6eb..2fe7a66cef 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/ConnectionsTable/ConnectionDetailsInfo/SSH.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { PlaceholderComponent, StaticImage, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { PlaceholderComponent, StaticImage, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; @@ -18,7 +18,7 @@ import { CONNECTION_DETAILS_STYLES } from './ConnectionDetailsStyles'; export const SSH: PlaceholderComponent = observer(function SSH({ connection }) { const translate = useTranslate(); - const handler = useMapResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID); + const handler = useResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID); const sshConfig = connection.networkHandlersConfig?.find(state => state.id === SSH_TUNNEL_ID); if (!sshConfig || !sshConfig.enabled) { diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx index 4fae69e594..6ed72a4c9c 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -9,11 +9,9 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { IconButton, Loader, StaticImage, Icon, useMapResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { IconButton, Loader, StaticImage, Icon, useResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; - - import { TabsState, TabList, UNDERLINE_TAB_STYLES, TabPanelList, BASE_TAB_STYLES } from '@cloudbeaver/core-ui'; import { ConnectionForm } from '@cloudbeaver/plugin-connections'; @@ -122,7 +120,7 @@ export const CreateConnection = observer(function CreateConnection({ const style = useStyles(componentStyle); const createConnectionService = useService(CreateConnectionService); const translate = useTranslate(); - const driver = useMapResource( + const driver = useResource( CreateConnection, DBDriverResource, createConnectionService.data?.config.driverId || null diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx index 55870093f8..b9e210b419 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Manual/CustomConnection.tsx @@ -10,7 +10,7 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; -import { Loader, useMapResource } from '@cloudbeaver/core-blocks'; +import { Loader, useResource } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -24,13 +24,13 @@ export const CustomConnection = observer(function CustomConnection() { const projectsService = useService(ProjectsService); const notificationService = useService(NotificationService); const connectionManualService = useService(ConnectionManualService); - const dbDriverResource = useMapResource(CustomConnection, DBDriverResource, CachedMapAllKey); + const dbDriverResource = useResource(CustomConnection, DBDriverResource, CachedMapAllKey); const drivers = useMemo(() => computed(() => ( dbDriverResource.resource.enabledDrivers.slice().sort(dbDriverResource.resource.compare) )), [dbDriverResource]); - useMapResource(CustomConnection, ProjectInfoResource, CachedMapAllKey); + useResource(CustomConnection, ProjectInfoResource, CachedMapAllKey); function select(driverId: string) { const shared = projectsService.activeProjects.filter(project => project.shared); diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index aae19b8e4e..f37d6160e3 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -15,7 +15,7 @@ import { TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; import { TextPlaceholder, Loader, - useMapResource, + useResource, BASE_CONTAINERS_STYLES, ColoredContainer, Group, @@ -62,8 +62,8 @@ export const ConnectionAccess: TabContainerPanelComponent useAutoLoad(state, selected); - const users = useMapResource(ConnectionAccess, UsersResource, CachedMapAllKey, { active: selected }); - const teams = useMapResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected }); + const users = useResource(ConnectionAccess, UsersResource, CachedMapAllKey, { active: selected }); + const teams = useResource(ConnectionAccess, TeamsResource, CachedMapAllKey, { active: selected }); const grantedUsers = useMemo(() => computed(() => users.resource.values .filter(user => state.state.grantedSubjects.includes(user.userId)) diff --git a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts index c2389c973d..8bf5c6725d 100644 --- a/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts +++ b/webapp/packages/plugin-connections-administration/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts @@ -14,7 +14,7 @@ import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk'; import { useTabState } from '@cloudbeaver/core-ui'; -import { isArraysEqual } from '@cloudbeaver/core-utils'; +import { isArraysEqual, isContainsException } from '@cloudbeaver/core-utils'; import type { IConnectionAccessTabState } from './IConnectionAccessTabState'; @@ -40,6 +40,9 @@ export function useConnectionAccessState(connection: DatabaseConnectionFragment isLoading() { return this.state.loading; }, + isError() { + return isContainsException(this.exception); + }, isLoaded() { return this.state.loaded; }, diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx index 1d03ca0813..00f2e3b035 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/ConnectionAuthenticationForm.tsx @@ -9,9 +9,8 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { BASE_CONTAINERS_STYLES, Container, FieldCheckbox, Group, GroupTitle, ObjectPropertyInfoForm, TextPlaceholder, useMapResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, Container, FieldCheckbox, Group, GroupTitle, ObjectPropertyInfoForm, TextPlaceholder, useResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource } from '@cloudbeaver/core-connections'; - import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; @@ -42,7 +41,7 @@ export const ConnectionAuthenticationForm = observer(function ConnectionA hideFeatures, }) { const translate = useTranslate(); - const authModel = useMapResource(ConnectionAuthenticationForm, DatabaseAuthModelsResource, authModelId); + const authModel = useResource(ConnectionAuthenticationForm, DatabaseAuthModelsResource, authModelId); let properties = authModel.data?.properties; diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx index 64ca349b64..7984518c06 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { BASE_CONTAINERS_STYLES, FieldCheckbox, GroupTitle, InputField, useMapResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, FieldCheckbox, GroupTitle, InputField, useResource, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource } from '@cloudbeaver/core-connections'; import { NetworkHandlerAuthType, NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; @@ -24,7 +24,7 @@ interface Props { export const NetworkHandlerAuthForm = observer(function NetworkHandlerAuthForm({ id, networkHandlersConfig, allowSaveCredentials, disabled }) { const translate = useTranslate(); - const handler = useMapResource(NetworkHandlerAuthForm, NetworkHandlerResource, id); + const handler = useResource(NetworkHandlerAuthForm, NetworkHandlerResource, id); //@TODO Do not mutate state in component body if (!networkHandlersConfig.some(state => state.id === id)) { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx index 6e29967118..51a8771443 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx @@ -9,9 +9,8 @@ import { observer } from 'mobx-react-lite'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { Button, getComputed, PlaceholderComponent, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, getComputed, PlaceholderComponent, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { DBDriverResource, DatabaseAuthModelsResource } from '@cloudbeaver/core-connections'; - import { useAuthenticationAction } from '@cloudbeaver/core-ui'; import type { IConnectionFormProps } from './IConnectionFormProps'; @@ -21,14 +20,14 @@ export const ConnectionFormBaseActions: PlaceholderComponent state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null) diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx index 456197980a..32fde09876 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -11,9 +11,8 @@ import { observer } from 'mobx-react-lite'; import { useMemo, useState } from 'react'; import styled, { css } from 'reshadow'; -import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, IProperty, Loader, PropertiesTable, useMapResource, useStyles } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, IProperty, Loader, PropertiesTable, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; - import { TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; import { uuid } from '@cloudbeaver/core-utils'; @@ -61,10 +60,10 @@ export const DriverProperties: TabContainerPanelComponent return { propertiesList, add, remove }; }); - const driver = useMapResource( + const driver = useResource( DriverProperties, DBDriverResource, - { key: (selected && formState.config.driverId) || null, includes: ['includeDriverProperties'] }, + { key: (selected && formState.config.driverId) || null, includes: ['includeDriverProperties'] as const }, { active: selected && !!formState.config.driverId, onData: driver => { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index 15fad36df5..16b1678bb1 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -15,7 +15,7 @@ import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { InputField, SubmittingForm, - useMapResource, + useResource, ColoredContainer, BASE_CONTAINERS_STYLES, Group, @@ -32,7 +32,6 @@ import { FormFieldDescription, useTranslate, usePermission, - useDataResource, } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; @@ -76,7 +75,7 @@ const driverConfiguration: IDriverConfiguration[] = [ export const Options: TabContainerPanelComponent = observer(function Options({ state, }) { - const serverConfigResource = useDataResource(Options, ServerConfigResource, undefined); + const serverConfigResource = useResource(Options, ServerConfigResource, undefined as void); const connectionOptionsTabService = useService(ConnectionOptionsTabService); const service = useService(ConnectionFormService); const formRef = useRef(null); @@ -117,11 +116,10 @@ export const Options: TabContainerPanelComponent = observe optionsHook.setAuthModel(model); }, []); - - const driverMap = useMapResource( + const driverMap = useResource( Options, DBDriverResource, - { key: config.driverId || null, includes: ['includeProviderProperties'] }, + { key: config.driverId || null, includes: ['includeProviderProperties'] as const }, { onData: (data, resource, prevData) => { if (data.id !== prevData?.id) { @@ -155,13 +153,13 @@ export const Options: TabContainerPanelComponent = observe } } - const { data: applicableAuthModels } = useMapResource( + const { data: applicableAuthModels } = useResource( Options, DatabaseAuthModelsResource, getComputed(() => driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedMapEmptyKey) ); - const { data: authModel } = useMapResource( + const { data: authModel } = useResource( Options, DatabaseAuthModelsResource, getComputed(() => config.authModelId || info?.authModel || driver?.defaultAuthModel || null), diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx index 83c272fc91..4cdfa8130a 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/ConnectionFormAuthenticationAction.tsx @@ -9,9 +9,8 @@ import { observer } from 'mobx-react-lite'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { Button, getComputed, PlaceholderComponent, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, getComputed, PlaceholderComponent, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource, DBDriverResource } from '@cloudbeaver/core-connections'; - import { useAuthenticationAction } from '@cloudbeaver/core-ui'; import type { IConnectionFormProps } from '../IConnectionFormProps'; @@ -20,14 +19,14 @@ export const AuthenticationButton: PlaceholderComponent = state, }) { const translate = useTranslate(); - const driverMap = useMapResource( + const driverMap = useResource( ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null ); const driver = driverMap.data; - const { data: authModel } = useMapResource( + const { data: authModel } = useResource( ConnectionFormAuthenticationAction, DatabaseAuthModelsResource, getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null) @@ -57,14 +56,14 @@ export const AuthenticationButton: PlaceholderComponent = export const ConnectionFormAuthenticationAction: PlaceholderComponent = observer(function ConnectionFormAuthenticationAction({ state, }) { - const driverMap = useMapResource( + const driverMap = useResource( ConnectionFormAuthenticationAction, DBDriverResource, state.config.driverId || null ); const driver = driverMap.data; - const { data: authModel } = useMapResource( + const { data: authModel } = useResource( ConnectionFormAuthenticationAction, DatabaseAuthModelsResource, getComputed(() => state.config.authModelId || state.info?.authModel || driver?.defaultAuthModel || null) diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx index 112d314c63..b32b7a713d 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/OriginInfo/OriginInfo.tsx @@ -9,10 +9,8 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { TextPlaceholder, Loader, ExceptionMessage, useMapResource, ColoredContainer, Group, ObjectPropertyInfoForm, BASE_CONTAINERS_STYLES, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { TextPlaceholder, Loader, ExceptionMessage, useResource, ColoredContainer, Group, ObjectPropertyInfoForm, BASE_CONTAINERS_STYLES, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { createConnectionParam } from '@cloudbeaver/core-connections'; - - import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; import type { IConnectionFormProps } from '../IConnectionFormProps'; @@ -43,9 +41,9 @@ export const OriginInfo: TabContainerPanelComponent = obse const state = useTabState>(); const styles = useStyles(style, BASE_CONTAINERS_STYLES); - const connection = useMapResource(OriginInfo, resource, { + const connection = useResource(OriginInfo, resource, { key: (tab.selected && info) ? createConnectionParam(info.projectId, info.id) : null, - includes: ['includeOrigin', 'customIncludeOriginDetails'], + includes: ['includeOrigin', 'customIncludeOriginDetails'] as const, }, { // isActive: () => !info?.origin || userInfoService.hasOrigin(info.origin), onData: (connection, res, prev) => { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx index 381fb6ed50..66f6db2dfd 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx @@ -12,7 +12,7 @@ import styled, { css } from 'reshadow'; import { useAdministrationSettings } from '@cloudbeaver/core-administration'; import { - Group, SubmittingForm, useMapResource, Button, ColoredContainer, InputField, + Group, SubmittingForm, useResource, Button, ColoredContainer, InputField, FieldCheckbox, BASE_CONTAINERS_STYLES, Switch, GroupItem, Container, Combobox, Expandable, EXPANDABLE_FORM_STYLES, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; @@ -51,7 +51,7 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ const initialConfig = info?.networkHandlersConfig?.find(handler => handler.id === SSH_TUNNEL_ID); - const resource = useMapResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID, { + const resource = useResource(SSH, NetworkHandlerResource, SSH_TUNNEL_ID, { onData: handler => { if (Object.keys(handlerState).length === 0) { for (const property of handler.properties) { diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx index cf91ac9192..91ddbb8963 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSHTab.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { Translate, useMapResource, useStyles } from '@cloudbeaver/core-blocks'; +import { Translate, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/core-connections'; import { TabTitle, Tab, TabContainerTabComponent } from '@cloudbeaver/core-ui'; @@ -20,7 +20,7 @@ export const SSHTab: TabContainerTabComponent = observer(f ...rest }) { const styles = useStyles(style); - const handler = useMapResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID); + const handler = useResource(SSHTab, NetworkHandlerResource, SSH_TUNNEL_ID); return styled(styles)( diff --git a/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx b/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx index 078cbd50b8..2ba0f97b0c 100644 --- a/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx +++ b/webapp/packages/plugin-data-export/src/Dialog/OutputOptionsForm.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { Combobox, FieldCheckbox, Loader, useDataResource } from '@cloudbeaver/core-blocks'; +import { Combobox, FieldCheckbox, Loader, useResource } from '@cloudbeaver/core-blocks'; import type { DataTransferOutputSettings } from '@cloudbeaver/core-sdk'; import { DefaultExportOutputSettingsResource } from './DefaultExportOutputSettingsResource'; @@ -35,7 +35,7 @@ interface Props { } export const OutputOptionsForm = observer(function OutputOptionsForm(props: Props) { - const resource = useDataResource(OutputOptionsForm, DefaultExportOutputSettingsResource, undefined); + const resource = useResource(OutputOptionsForm, DefaultExportOutputSettingsResource, undefined); return ( diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts index 3d00305c66..018adebf95 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerPage/useDataViewerDatabaseDataModel.ts @@ -9,7 +9,7 @@ import { action, computed, observable } from 'mobx'; import { useEffect } from 'react'; -import { useMapResource, useObservableRef } from '@cloudbeaver/core-blocks'; +import { useResource, useObservableRef } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree'; @@ -38,7 +38,7 @@ export function useDataViewerDatabaseDataModel(tab: ITab) const dataPresentationService = useService(DataPresentationService); const dataViewerDataChangeConfirmationService = useService(DataViewerDataChangeConfirmationService); - const connection = useMapResource( + const connection = useResource( useDataViewerDatabaseDataModel, ConnectionInfoResource, tab.handlerState.connectionKey ?? null 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 0480bc5910..3bb0dace31 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 @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css, use } from 'reshadow'; -import { ConnectionMark, IconOrImage, useMapResource, useStyles } from '@cloudbeaver/core-blocks'; +import { ConnectionMark, IconOrImage, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { DBDriverResource, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; @@ -45,14 +45,14 @@ export const ConnectionIcon: React.FC = observer(function ConnectionInfo( }) { const styles = useStyles(style, connectionIconStyle); - const connection = useMapResource( + const connection = useResource( ConnectionInfo, ConnectionInfoResource, connectionKey ?? null ); const driverId = connection.data?.driverId; - const driver = useMapResource(ConnectionInfo, DBDriverResource, driverId!, { + const driver = useResource(ConnectionInfo, DBDriverResource, driverId!, { active: driverId !== undefined, }); diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index b8a5356561..f4c535472a 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -189,7 +189,8 @@ export class PluginBootstrap extends Bootstrap { }, { onSelect: () => { - const instance = this.diService.serviceInjector.getServiceByClass>( + const instance = this.diService.serviceInjector + .getServiceByClass>( item.resource ); instance.markOutdated(undefined); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx index f09d7fa120..92ae314f20 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTree.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useMemo, useCallback, useEffect } from 'react'; import styled, { css, use } from 'reshadow'; -import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag, FolderExplorer, FolderExplorerPath, Loader, PlaceholderElement, Translate, TreeNodeNested, TreeNodeNestedMessage, TREE_NODE_STYLES, useFolderExplorer, useMapResource, useObjectRef, useStyles } from '@cloudbeaver/core-blocks'; +import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag, FolderExplorer, FolderExplorerPath, Loader, PlaceholderElement, Translate, TreeNodeNested, TreeNodeNestedMessage, TREE_NODE_STYLES, useFolderExplorer, useResource, useObjectRef, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { type NavNode, ROOT_NODE_PATH, NavTreeResource, NavNodeInfoResource, EObjectFeature } from '@cloudbeaver/core-navigation-tree'; @@ -177,7 +177,7 @@ export const ElementsTree = observer(function ElementsTree({ }, [folderExplorer]); - useMapResource(ElementsTree, navTreeResource, root, { + useResource(ElementsTree, navTreeResource, root, { onLoad: async resource => { let fullPath = folderExplorer.state.fullPath; const preload = await resource.preloadNodeParents(fullPath); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx index e4fc93f758..a74dae6ac8 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import React, { useState } from 'react'; import styled, { css, use } from 'reshadow'; -import { IconButton, PlaceholderElement, useMapResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { IconButton, PlaceholderElement, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { NavTreeResource } from '@cloudbeaver/core-navigation-tree'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { useCaptureViewContext } from '@cloudbeaver/core-view'; @@ -88,7 +88,7 @@ export const ElementsTreeTools = observer>(functi const translate = useTranslate(); const [opened, setOpen] = useState(false); const styles = useStyles(toolsStyles, style); - const rootNode = useMapResource(ElementsTreeTools, NavTreeResource, root); + const rootNode = useResource(ElementsTreeTools, NavTreeResource, root); useCaptureViewContext(context => { context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index d27d80effd..b3e854cb8d 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -9,7 +9,7 @@ import { action, computed, observable, runInAction } from 'mobx'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { IFolderExplorerContext, useExecutor, useMapResource, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks'; +import { IFolderExplorerContext, useExecutor, useResource, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -578,7 +578,7 @@ export function useElementsTree(options: IOptions): IElementsTree { functionsRef.loadTree(options.root); }, 100), []); - useMapResource(useElementsTree, ProjectInfoResource, CachedMapAllKey, { + useResource(useElementsTree, ProjectInfoResource, CachedMapAllKey, { onData: () => { loadTreeThreshold(); }, diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx index fea6469d58..b51f7f3787 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -47,7 +47,7 @@ const styles = css` portal { box-sizing: border-box; margin-left: auto !important; - margin-right: 16px !important; + margin-right: 8px !important; visibility: hidden; } name-box { diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx index 3fbbefbada..beb11723a2 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoForm, Group, useMapResource, BASE_CONTAINERS_STYLES, useTranslate } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoForm, Group, useResource, BASE_CONTAINERS_STYLES, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavTreeResource, DBObjectResource } from '@cloudbeaver/core-navigation-tree'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; @@ -28,7 +28,7 @@ export const ObjectProperties = observer(function ObjectProperties({ }) { const translate = useTranslate(); const navTreeResource = useService(NavTreeResource); - const dbObject = useMapResource(ObjectProperties, DBObjectResource, objectId, { + const dbObject = useResource(ObjectProperties, DBObjectResource, objectId, { onLoad: async () => !(await navTreeResource.preloadNodeParents(parents, objectId)), }); const { categories, isUncategorizedExists } = useObjectPropertyCategories( diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx index 0f50d56d04..4fc6dea785 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx @@ -11,7 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, TextPlaceholder, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavNodeInfoResource, NavTreeResource, DBObjectResource, type DBObject, DBObjectParentKey } from '@cloudbeaver/core-navigation-tree'; import { type NavNodeTransformViewComponent, NavNodeViewService } from '@cloudbeaver/plugin-navigation-tree'; @@ -38,7 +38,7 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi const nodeType = VirtualFolderUtils.getNodeType(folderId); const navNodeViewService = useService(NavNodeViewService); const navNodeInfoResource = useService(NavNodeInfoResource); - const tree = useMapResource(VirtualFolderPanel, NavTreeResource, nodeId, { + const tree = useResource(VirtualFolderPanel, NavTreeResource, nodeId, { onLoad: async resource => !(await resource.preloadNodeParents(parents, nodeId)), }); @@ -46,7 +46,7 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi const { nodes, duplicates } = navNodeViewService.filterDuplicates(limited.nodes); - const dbObject = useMapResource(VirtualFolderPanel, DBObjectResource, DBObjectParentKey(nodeId), { + const dbObject = useResource(VirtualFolderPanel, DBObjectResource, DBObjectParentKey(nodeId), { preload: [tree], }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx index f122dfa423..9ca0d52171 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, TextPlaceholder, useMapResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavNodeManagerService, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; import { BASE_TAB_STYLES, ITabData, TabList, TabPanel, TabsState, useTabLocalState, verticalTabStyles } from '@cloudbeaver/core-ui'; @@ -72,7 +72,7 @@ export const ObjectFolders = observer(function ObjectFolders({ tab }) { const parents = tab.handlerState.parents; let folderId = tab.handlerState.folderId; - const children = useMapResource(ObjectFolders, NavTreeResource, nodeId, { + const children = useResource(ObjectFolders, NavTreeResource, nodeId, { onLoad: async resource => !(await resource.preloadNodeParents(parents, nodeId)), }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index 75906ad607..607f1ff9ab 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -11,7 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, TextPlaceholder, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavTreeResource, DBObjectResource, type DBObject, DBObjectParentKey } from '@cloudbeaver/core-navigation-tree'; import { NavNodeViewService } from '@cloudbeaver/plugin-navigation-tree'; @@ -44,7 +44,7 @@ export const ObjectPropertyTable = observer(function O parents = [...parents, parentId]; const translate = useTranslate(); const navNodeViewService = useService(NavNodeViewService); - const tree = useMapResource(ObjectPropertyTable, NavTreeResource, objectId, { + const tree = useResource(ObjectPropertyTable, NavTreeResource, objectId, { onLoad: async resource => !(await resource.preloadNodeParents(parents, objectId)), }); @@ -52,7 +52,7 @@ export const ObjectPropertyTable = observer(function O const { nodes, duplicates } = navNodeViewService.filterDuplicates(limited.nodes); - const dbObject = useMapResource(ObjectPropertyTable, DBObjectResource, DBObjectParentKey(objectId), { + const dbObject = useResource(ObjectPropertyTable, DBObjectResource, DBObjectParentKey(objectId), { preload: [tree], }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx index 4fa56ff898..98aba693f3 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx @@ -11,7 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, TextPlaceholder, Button, useMapResource, useObservableRef, getComputed, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, Button, useResource, useObservableRef, getComputed, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { ConnectionInfoResource, ConnectionsManagerService } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; @@ -62,13 +62,13 @@ export const ObjectViewerPanel: TabHandlerPanelComponent notFound: observable.ref, }, false); - const connection = useMapResource(ObjectViewerPanel, ConnectionInfoResource, connectionKey, { + const connection = useResource(ObjectViewerPanel, ConnectionInfoResource, connectionKey, { isActive: resource => !connectionKey || !resource.has(connectionKey), }); const connected = getComputed(() => connection.data?.connected || false); - const children = useMapResource(ObjectViewerPanel, NavTreeResource, parentId, { + const children = useResource(ObjectViewerPanel, NavTreeResource, parentId, { onLoad: async resource => { if (!connected) { return true; @@ -85,7 +85,7 @@ export const ObjectViewerPanel: TabHandlerPanelComponent preload: [connection], }); - const node = useMapResource(ObjectViewerPanel, navNodeInfoResource, objectId, { + const node = useResource(ObjectViewerPanel, navNodeInfoResource, objectId, { onLoad: async () => !(await children.resource.preloadNodeParents(parents, objectId)), onData(data) { tab.handlerState.tabIcon = data.icon; diff --git a/webapp/packages/plugin-projects/src/ProjectSelect.tsx b/webapp/packages/plugin-projects/src/ProjectSelect.tsx index c7f3c036b6..cadd1ee1a1 100644 --- a/webapp/packages/plugin-projects/src/ProjectSelect.tsx +++ b/webapp/packages/plugin-projects/src/ProjectSelect.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { Combobox, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Combobox, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { ProjectInfo, ProjectInfoResource, projectInfoSortByName, ProjectsService } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; @@ -45,7 +45,7 @@ export const ProjectSelect = observer(function ProjectSelect({ .filter(filter) .map(project => project.id); - const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, { + const projectsLoader = useResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, { onData: () => { if ( (!value && possibleOptions.length > 0) diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx b/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx index e2ee476f3e..d7e0e02fdc 100644 --- a/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx +++ b/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; -import { Loader, useDataResource, useTranslate, useUserData } from '@cloudbeaver/core-blocks'; +import { Loader, useResource, useTranslate, useUserData } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; import { ProjectsService } from '@cloudbeaver/core-projects'; @@ -77,7 +77,7 @@ export const ResourceManagerTree = observer(function ResourceManagerTree() { validateElementsTreeSettings ); - const { resource } = useDataResource(ResourceManagerTree, ResourceProjectsResource, undefined); + const resourceProjectsLoader = useResource(ResourceManagerTree, ResourceProjectsResource, undefined); const projectsRendererRenderer = useMemo( () => navigationTreeProjectsRendererRenderer(navNodeInfoResource), @@ -113,7 +113,7 @@ export const ResourceManagerTree = observer(function ResourceManagerTree() { const settingsElements = useMemo(() => ([ProjectsSettingsPlaceholderElement]), []); return styled(styles)( - + { if (this.state.resourceKey && !this.resourceUseKeyId) { - this.resourceUseKeyId = this.resourceManagerResource.use(this.state.resourceKey); + this.resourceUseKeyId = this.resourceManagerResource.use(toJS(this.state.resourceKey)); } await this.read(); await this.updateProperties(); diff --git a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx index c9eee18ac2..b315721712 100644 --- a/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx +++ b/webapp/packages/plugin-sql-editor-screen/src/Screen/SqlEditorScreen.tsx @@ -9,7 +9,7 @@ import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { Loader, TextPlaceholder, useMapResource, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, useResource, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; import { ConnectionExecutionContextResource, ConnectionExecutionContextService, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import type { ScreenComponent } from '@cloudbeaver/core-routing'; @@ -23,7 +23,7 @@ export const SqlEditorScreen: ScreenComponent = observer contextId, }) { const translate = useTranslate(); - const connectionExecutionContextResource = useMapResource( + const connectionExecutionContextResource = useResource( SqlEditorScreen, ConnectionExecutionContextResource, CachedMapAllKey diff --git a/webapp/packages/plugin-sql-editor/src/ScriptPreview/ScriptPreviewDialog.tsx b/webapp/packages/plugin-sql-editor/src/ScriptPreview/ScriptPreviewDialog.tsx index 91bd1c4a50..018728b35c 100644 --- a/webapp/packages/plugin-sql-editor/src/ScriptPreview/ScriptPreviewDialog.tsx +++ b/webapp/packages/plugin-sql-editor/src/ScriptPreview/ScriptPreviewDialog.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { Button, useClipboard, useMapResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { Button, useClipboard, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { ConnectionDialectResource, ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; @@ -54,7 +54,7 @@ export const ScriptPreviewDialog = observer>(funct const connectionExecutionContextService = useService(ConnectionExecutionContextService); const context = connectionExecutionContextService.get(payload.model.source.executionContext?.context?.id ?? ''); const contextInfo = context?.context; - const dialect = useMapResource(ScriptPreviewDialog, ConnectionDialectResource, contextInfo + const dialect = useResource(ScriptPreviewDialog, ConnectionDialectResource, contextInfo ? createConnectionParam(contextInfo.projectId, contextInfo.connectionId) : null ); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts index 89d8905da3..8897efa858 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts @@ -10,6 +10,7 @@ import { makeObservable, observable } from 'mobx'; import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { isContainsException } from '@cloudbeaver/core-utils'; import type { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures'; import type { ISqlDataSource } from './ISqlDataSource'; @@ -56,6 +57,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { abstract setName(name: string | null): void; abstract setExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void; + isError(): boolean { + return isContainsException(this.exception); + } + isEditing(): boolean { return this.editing; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx index d2a4f9a749..9becd6f726 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; import styled, { css } from 'reshadow'; -import { splitStyles, Split, ResizerControls, Pane, splitHorizontalStyles, Overlay, OverlayMessage, OverlayActions, Button, useMapResource, getComputed, OverlayHeader, OverlayHeaderIcon, OverlayHeaderTitle, OverlayHeaderSubTitle, useSplitUserState, Loader, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { splitStyles, Split, ResizerControls, Pane, splitHorizontalStyles, Overlay, OverlayMessage, OverlayActions, Button, useResource, getComputed, OverlayHeader, OverlayHeaderIcon, OverlayHeaderTitle, OverlayHeaderSubTitle, useSplitUserState, Loader, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { ConnectionExecutionContextResource, ConnectionInfoResource, createConnectionParam, DBDriverResource, getRealExecutionContextId } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; @@ -70,19 +70,19 @@ export const SqlEditor = observer(function SqlEditor({ state }) { const styles = useStyles(splitStyles, splitHorizontalStyles, viewerStyles); const dataSource = sqlDataSourceService.get(state.editorId); useDataSource(dataSource); - const connection = useMapResource( + const connection = useResource( SqlEditor, ConnectionInfoResource, dataSource?.executionContext ? createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId) : null ); - const driver = useMapResource(SqlEditor, DBDriverResource, connection.data?.driverId ?? null); + const driver = useResource(SqlEditor, DBDriverResource, connection.data?.driverId ?? null); const splitState = useSplitUserState('sql-editor'); const connected = getComputed(() => connection.data?.connected ?? false); - const context = useMapResource( + const context = useResource( SqlEditor, ConnectionExecutionContextResource, connected ? getRealExecutionContextId(dataSource?.executionContext?.id) : null diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx index cb8541484c..daa84a45cf 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { IconOrImage, Link, Cell, useMapResource, Loader, getComputed, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; +import { IconOrImage, Link, Cell, useResource, Loader, getComputed, useTranslate, useStyles } from '@cloudbeaver/core-blocks'; import { resourceKeyList, UserInfo } from '@cloudbeaver/core-sdk'; @@ -45,7 +45,7 @@ interface Props { } export const AuthProvidersList = observer(function AuthProvidersList({ user, providers, className }) { - const providersResource = useMapResource(AuthProvidersList, AuthProvidersResource, resourceKeyList(providers)); + const providersResource = useResource(AuthProvidersList, AuthProvidersResource, resourceKeyList(providers)); const translate = useTranslate(); const style = useStyles(styles); const activeProviders = getComputed(() => user.authTokens.map(token => token.authProvider)); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx index 96c186ec03..e70b891376 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { AuthProviderConfiguration, AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { IconOrImage, Link, Cell, useMapResource, Loader, Button, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { IconOrImage, Link, Cell, useResource, Loader, Button, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey, UserInfo } from '@cloudbeaver/core-sdk'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; @@ -48,7 +48,7 @@ interface Props { } export const AuthTokenList = observer(function AuthTokenList({ user, className }) { - const providersResource = useMapResource(AuthTokenList, AuthProvidersResource, CachedMapAllKey); + const providersResource = useResource(AuthTokenList, AuthProvidersResource, CachedMapAllKey); const authenticationService = useService(AuthenticationService); const style = useStyles(styles); const translate = useTranslate(); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx index 4eb1edb163..f2e13a4ff0 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; import { UserMetaParametersResource } from '@cloudbeaver/core-authentication'; -import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupTitle, InputField, Loader, ObjectPropertyInfoForm, useDataResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupTitle, InputField, Loader, ObjectPropertyInfoForm, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import type { UserInfo } from '@cloudbeaver/core-sdk'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { BASE_TAB_STYLES, TabPanel } from '@cloudbeaver/core-ui'; @@ -28,7 +28,7 @@ export const UserInfoPanel = observer(function UserInfoPanel({ className, style, }) { - const userMetaParameters = useDataResource(UserInfoPanel, UserMetaParametersResource, undefined); + const userMetaParameters = useResource(UserInfoPanel, UserMetaParametersResource, undefined); const styles = useStyles(BASE_TAB_STYLES, style, BASE_CONTAINERS_STYLES); const translate = useTranslate(); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx index a791843548..7b4af5ac5b 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx @@ -11,7 +11,7 @@ import { useCallback } from 'react'; import styled, { css } from 'reshadow'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { Loader, useDataResource } from '@cloudbeaver/core-blocks'; +import { Loader, useResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { UserProfileService } from '../UserProfileService'; @@ -25,10 +25,11 @@ const style = css` export const UserProfileForm = observer(function UserProfileForm() { const userProfileService = useService(UserProfileService); - const userInfo = useDataResource(UserProfileForm, UserInfoResource, { - key: undefined as any, - includes: ['includeMetaParameters'], + const userInfo = useResource(UserProfileForm, UserInfoResource, { + key: undefined, + includes: ['includeMetaParameters'] as const, }); + const close = useCallback(() => userProfileService.close(true), []); if (!userProfileService.formState) { diff --git a/webapp/packages/plugin-version-update-administration/src/VersionInfo.tsx b/webapp/packages/plugin-version-update-administration/src/VersionInfo.tsx index 2ffaa52f13..87fc5bb77b 100644 --- a/webapp/packages/plugin-version-update-administration/src/VersionInfo.tsx +++ b/webapp/packages/plugin-version-update-administration/src/VersionInfo.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import ReactMarkdown from 'react-markdown'; import styled, { css } from 'reshadow'; -import { BASE_CONTAINERS_STYLES, Container, Group, GroupItem, GroupTitle, useMapResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, Container, Group, GroupItem, GroupTitle, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { VersionResource } from '@cloudbeaver/core-version'; interface Props { @@ -32,7 +32,7 @@ const style = css` export const VersionInfo = observer(function VersionInfo({ item }) { const translate = useTranslate(); const styles = useStyles(BASE_CONTAINERS_STYLES, style); - const versionResource = useMapResource(VersionInfo, VersionResource, item); + const versionResource = useResource(VersionInfo, VersionResource, item); const version = versionResource.data; diff --git a/webapp/packages/plugin-version-update-administration/src/VersionUpdate.tsx b/webapp/packages/plugin-version-update-administration/src/VersionUpdate.tsx index d939ddf3ad..b5064df488 100644 --- a/webapp/packages/plugin-version-update-administration/src/VersionUpdate.tsx +++ b/webapp/packages/plugin-version-update-administration/src/VersionUpdate.tsx @@ -11,7 +11,7 @@ import styled, { css } from 'reshadow'; import { gte } from 'semver'; import type { AdministrationItemContentComponent } from '@cloudbeaver/core-administration'; -import { BASE_CONTAINERS_STYLES, ColoredContainer, useMapResource, useStyles } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { VersionResource, VersionService } from '@cloudbeaver/core-version'; @@ -31,7 +31,7 @@ const styles = css` export const VersionUpdate: AdministrationItemContentComponent = observer(function VersionUpdate() { const style = useStyles(BASE_CONTAINERS_STYLES, styles); const versionService = useService(VersionService); - const versionResource = useMapResource(VersionUpdate, VersionResource, CachedMapAllKey, { + const versionResource = useResource(VersionUpdate, VersionResource, CachedMapAllKey, { silent: true, }); diff --git a/webapp/packages/plugin-version-update-administration/src/VersionUpdateDrawerItem.tsx b/webapp/packages/plugin-version-update-administration/src/VersionUpdateDrawerItem.tsx index 392bb6a1f1..9e06a50f09 100644 --- a/webapp/packages/plugin-version-update-administration/src/VersionUpdateDrawerItem.tsx +++ b/webapp/packages/plugin-version-update-administration/src/VersionUpdateDrawerItem.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration'; -import { Translate, useMapResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Translate, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { Tab, TabTitle, TabIcon } from '@cloudbeaver/core-ui'; @@ -43,7 +43,7 @@ export const VersionUpdateDrawerItem = observer(f const translate = useTranslate(); const versionUpdateService = useService(VersionUpdateService); - useMapResource(VersionUpdateDrawerItem, VersionResource, CachedMapAllKey); + useResource(VersionUpdateDrawerItem, VersionResource, CachedMapAllKey); return styled(useStyles(style, styles))(