From 39987507a53d1bfea4bc29df8ef842e099b433e9 Mon Sep 17 00:00:00 2001 From: Oksana Cherniavskaia <114406033+oksana-cherniavskaia@users.noreply.github.com> Date: Mon, 17 Oct 2022 23:06:28 +0300 Subject: [PATCH] CB-2660 add set resource property mutation to resource manager updated (#1214) * CB-2660: Add set property mutation to resource manager * CB-2660 fix: save resource property * CB-2660 Modify graphql schema to allow properties in RMResource to be null * CB-2660 fix: resource properties keys * CB-2660 fix: codestyle * CB-2660 Read resource properties in navigator model * CB-2660 Respect nameMask parameter * CB-2660 feat: bind execution context information * CB-2660 fix: don't save properties for readonly scripts * CB-2660 fix: don't link readonly scripts with datasource * CB-2660 fix: script linking with datasource * CB-2660 fix: sql editor context switch flickering * CB-2660 fix(plugin-resource-manager): resource finding Co-authored-by: Wroud Co-authored-by: serge-rider Co-authored-by: Alexey --- .../model/rm/DBNResourceManagerProject.java | 2 +- .../model/rm/DBNResourceManagerResource.java | 2 +- .../rm/local/LocalResourceController.java | 15 +- .../schema/service.rm.graphqls | 3 + .../cloudbeaver/service/rm/DBWServiceRM.java | 19 ++ .../service/rm/WebServiceBindingRM.java | 9 +- .../service/rm/impl/WebServiceRM.java | 63 +++- .../ConnectionExecutionContextResource.ts | 12 +- .../src/ConnectionInfoResource.ts | 6 +- .../src/TaskScheduler/TaskScheduler.ts | 2 +- .../src/NodesManager/NavTreeResource.ts | 2 + .../src/extensions/IProjectProvider.ts | 21 ++ .../src/extensions/IProjectSetter.ts | 25 ++ .../src/extensions/IProjectSetterState.ts | 22 ++ webapp/packages/core-projects/src/index.ts | 3 + .../core-sdk/src/Resource/CachedResource.ts | 8 +- .../src/queries/fragments/NavNodeInfo.gql | 1 + .../resource-manager/getResourceList.gql | 21 +- .../resource-manager/setResourceProperty.gql | 13 + webapp/packages/core-sdk/src/sdk.ts | 47 ++- .../packages/core-utils/src/isValuesEqual.ts | 2 +- .../ConnectionSchemaManagerBootstrap.ts | 12 +- .../ConnectionSchemaManagerService.ts | 62 ++++ .../plugin-projects/src/ProjectSelect.tsx | 5 +- .../src/NavResourceNodeService.ts | 17 ++ .../src/ResourceManagerResource.ts | 93 +++++- .../src/SaveScriptDialog.tsx | 5 +- .../src/IResourceSqlDataSourceState.ts | 1 + .../src/PluginBootstrap.ts | 39 ++- .../src/ResourceSqlDataSource.ts | 282 +++++++++++++----- .../src/ResourceSqlDataSourceBootstrap.ts | 58 +++- .../src/SqlEditorTabService.ts | 32 +- .../src/SqlDataSource/BaseSqlDataSource.ts | 9 + .../SqlDataSource/ESqlDataSourceFeatures.ts | 3 +- .../src/SqlDataSource/ISqlDataSource.ts | 2 + .../plugin-sql-editor/src/SqlEditor.tsx | 11 +- 36 files changed, 783 insertions(+), 146 deletions(-) create mode 100644 webapp/packages/core-projects/src/extensions/IProjectProvider.ts create mode 100644 webapp/packages/core-projects/src/extensions/IProjectSetter.ts create mode 100644 webapp/packages/core-projects/src/extensions/IProjectSetterState.ts create mode 100644 webapp/packages/core-sdk/src/queries/resource-manager/setResourceProperty.gql diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java index 127f10e8f7..0c91d030e2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerProject.java @@ -91,7 +91,7 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode { if (children == null) { List rfList = new ArrayList<>(); for (RMResource resource : getResourceController().listResources( - project.getId(), null, null, false, false, false)) { + project.getId(), null, null, true, false, false)) { rfList.add(new DBNResourceManagerResource(this, resource)); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java index 832ccee853..1ab083835d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/DBNResourceManagerResource.java @@ -76,7 +76,7 @@ public class DBNResourceManagerResource extends DBNAbstractResourceManagerNode { if (children == null) { List rfList = new ArrayList<>(); for (RMResource resource : getResourceController().listResources( - getResourceProject().getId(), getResourceFolder(), null, false, false, false)) { + getResourceProject().getId(), getResourceFolder(), null, true, false, false)) { rfList.add(new DBNResourceManagerResource(this, resource)); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 918414cf82..8f6d3b9457 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -347,7 +347,7 @@ public class LocalResourceController implements RMController { if (!folderPath.startsWith(projectPath)) { throw new DBException("Invalid folder path"); } - return readChildResources(projectId, folderPath, readProperties, readHistory, recursive); + return readChildResources(projectId, folderPath, nameMask, readProperties, readHistory, recursive); } catch (NoSuchFileException e) { throw new DBException("Invalid resource folder " + folder); } catch (IOException e) { @@ -359,14 +359,18 @@ public class LocalResourceController implements RMController { private RMResource[] readChildResources( @NotNull String projectId, @NotNull Path folderPath, + @Nullable String nameMask, boolean readProperties, boolean readHistory, boolean recursive ) throws IOException { try (Stream files = Files.list(folderPath)) { - return files.filter(path -> !path.getFileName().toString().startsWith(".")) // skip hidden files + return files.filter(path -> { + String fileName = path.getFileName().toString(); + return (nameMask == null || nameMask.equals(fileName)) && !fileName.startsWith("."); + }) // skip hidden files .sorted(Comparator.comparing(path -> path.getFileName().toString(), String.CASE_INSENSITIVE_ORDER)) - .map((Path path) -> makeResourceFromPath(projectId, path, readProperties, readHistory, recursive)) + .map((Path path) -> makeResourceFromPath(projectId, path, nameMask, readProperties, readHistory, recursive)) .filter(Objects::nonNull) .toArray(RMResource[]::new); } @@ -631,7 +635,7 @@ public class LocalResourceController implements RMController { for (var resourceName : relativeResourcePath) { resourcePath = resourcePath.resolve(resourceName); - result.add(makeResourceFromPath(projectId, resourcePath, false, false, recursive)); + result.add(makeResourceFromPath(projectId, resourcePath, null, false, false, recursive)); } return result; @@ -640,6 +644,7 @@ public class LocalResourceController implements RMController { private RMResource makeResourceFromPath( @NotNull String projectId, @NotNull Path path, + @Nullable String nameMask, boolean readProperties, boolean readHistory, boolean recursive @@ -687,7 +692,7 @@ public class LocalResourceController implements RMController { if (recursive && resource.isFolder()) { try { - resource.setChildren(readChildResources(projectId, path, readProperties, readHistory, true)); + resource.setChildren(readChildResources(projectId, path, nameMask, readProperties, readHistory, true)); } catch (IOException e) { log.error(e); } diff --git a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls index d8c15e30c7..310319066d 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -15,6 +15,8 @@ type RMResource { name: String! folder: Boolean! length: Int! + # Properties map + properties: Object } input RMSubjectProjectPermissions { @@ -90,4 +92,5 @@ extend type Mutation { rmSetSubjectProjectPermissions(subjectId: String!, permissions: [RMProjectPermissions!]!): Boolean! + rmSetResourceProperty(projectId: String!, resourcePath: String!, name: ID!, value: String): Boolean! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java index 1e2b1c1297..c277cf30e7 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/DBWServiceRM.java @@ -58,6 +58,25 @@ public interface DBWServiceRM extends DBWService { boolean readProperties, boolean readHistory) throws DBException; + /** + * Sets resource property. + * + * @param webSession the web session + * @param projectId the project id + * @param resourcePath the resource path + * @param propertyName the property name + * @param propertyValue the property value + * @return the resource property + * @throws DBException the db exception + */ + @WebProjectAction(requireProjectPermissions = RMConstants.PERMISSION_PROJECT_RESOURCE_EDIT) + String setResourceProperty( + @NotNull WebSession webSession, + @NotNull @WebObjectId String projectId, + @NotNull String resourcePath, + @NotNull String propertyName, + @Nullable Object propertyValue) throws DBException; + @WebProjectAction( requireProjectPermissions = RMConstants.PERMISSION_PROJECT_RESOURCE_VIEW ) diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java index 794557dc2d..d4b24f5d4e 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java @@ -81,7 +81,6 @@ public class WebServiceBindingRM extends WebServiceBindingBase { env.getArgument("resourcePath"), env.getArgument("data"), env.getArgument("forceOverwrite"))) - .dataFetcher("rmCreateProject", env -> getService(env).createProject( getWebSession(env), env.getArgument("projectName"), @@ -96,12 +95,18 @@ public class WebServiceBindingRM extends WebServiceBindingBase { env.getArgument("projectId"), new RMSubjectProjectPermissions(env.getArgument("permissions")) )) + .dataFetcher("rmSetResourceProperty", env -> getService(env).setResourceProperty( + getWebSession(env), + env.getArgument("projectId"), + env.getArgument("resourcePath"), + env.getArgument("name"), + env.getArgument("value") + )) .dataFetcher("rmSetSubjectProjectPermissions", env -> getService(env).setSubjectProjectPermissions( getWebSession(env), env.getArgument("subjectId"), new RMProjectPermissions(env.getArgument("permissions")) )) ; - } } 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 35e7adca28..5b8864e406 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 @@ -43,7 +43,7 @@ import java.util.Set; public class WebServiceRM implements DBWServiceRM { @Override - public RMProject[] listProjects(WebSession webSession) throws DBWebException { + public RMProject[] listProjects(@NotNull WebSession webSession) throws DBWebException { try { return getResourceController(webSession).listAccessibleProjects(); } catch (DBException e) { @@ -71,7 +71,13 @@ public class WebServiceRM implements DBWServiceRM { @NotNull @Override - public RMResource[] listResources(WebSession webSession, @NotNull String projectId, @Nullable String folder, @Nullable String nameMask, boolean readProperties, boolean readHistory) throws DBException { + public RMResource[] listResources(@NotNull WebSession webSession, + @NotNull String projectId, + @Nullable String folder, + @Nullable String nameMask, + boolean readProperties, + boolean readHistory + ) throws DBException { checkIsRmEnabled(webSession); try { return getResourceController(webSession).listResources(projectId, folder, nameMask, readProperties, readHistory, false); @@ -80,8 +86,40 @@ public class WebServiceRM implements DBWServiceRM { } } + /** + * Sets resource property. + * + * @param webSession the web session + * @param projectId the project id + * @param resourcePath the resource path + * @param propertyName the property name + * @param propertyValue the property value + * @return the resource property + * @throws DBException the db exception + */ + @NotNull @Override - public String readResourceAsString(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath) throws DBException { + public String setResourceProperty(@NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String resourcePath, + @NotNull String propertyName, + @Nullable Object propertyValue + ) throws DBException { + checkIsRmEnabled(webSession); + try { + getResourceController(webSession).setResourceProperty(projectId, resourcePath, propertyName, propertyValue); + return Boolean.TRUE.toString(); + } catch (DBException e) { + String message = String.format("Error setting property [%s] for the the resource: [%s]", resourcePath, propertyName); + throw new DBWebException(message, e); + } + } + + @Override + public String readResourceAsString(@NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String resourcePath + ) throws DBException { checkIsRmEnabled(webSession); try { byte[] data = getResourceController(webSession).getResourceContents(projectId, resourcePath); @@ -92,7 +130,11 @@ public class WebServiceRM implements DBWServiceRM { } @Override - public String createResource(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath, boolean isFolder) throws DBException { + public String createResource(@NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String resourcePath, + boolean isFolder + ) throws DBException { checkIsRmEnabled(webSession); try { return getResourceController(webSession).createResource(projectId, resourcePath, isFolder); @@ -102,7 +144,10 @@ public class WebServiceRM implements DBWServiceRM { } @Override - public boolean deleteResource(@NotNull WebSession webSession, @NotNull String projectId, @NotNull String resourcePath) throws DBException { + public boolean deleteResource(@NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String resourcePath + ) throws DBException { checkIsRmEnabled(webSession); try { getResourceController(webSession).deleteResource(projectId, resourcePath, false); @@ -215,7 +260,9 @@ public class WebServiceRM implements DBWServiceRM { } @Override - public List listProjectGrantedPermissions(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException { + public List listProjectGrantedPermissions(@NotNull WebSession webSession, + @NotNull String projectId + ) throws DBWebException { SMController sm = webSession.getSecurityController(); try { return sm.getObjectPermissionGrants(projectId, SMObjects.PROJECT); @@ -225,7 +272,9 @@ public class WebServiceRM implements DBWServiceRM { } @Override - public List listSubjectProjectsPermissionGrants(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException { + public List listSubjectProjectsPermissionGrants(@NotNull WebSession webSession, + @NotNull String subjectId + ) throws DBWebException { try { SMAdminController sm = webSession.getAdminSecurityController(); return sm.getSubjectObjectPermissionGrants(subjectId, SMObjects.PROJECT); diff --git a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts index 281337fb2f..c2f3556bf2 100644 --- a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts +++ b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts @@ -33,6 +33,8 @@ export const ConnectionExecutionContextProjectKey = (projectId: string) => resou projectId ); +export const NOT_INITIALIZED_CONTEXT_ID = '-1'; + @injectable() export class ConnectionExecutionContextResource extends CachedMapResource { constructor( @@ -157,7 +159,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource ): Promise> { let projectId: string | undefined; - const all = this.includes(originalKey, CachedMapAllKey); + const all = this.isAliasEqual(originalKey, CachedMapAllKey); const isProjectFolders = isConnectionExecutionContextProjectKey(originalKey); originalKey = this.transformParam(originalKey); @@ -260,3 +262,11 @@ function isConnectionExecutionContextProjectKey( ): param is ResourceKeyList { return isResourceKeyList(param) && param.list.includes(connectionExecutionContextProjectKeySymbol); } + +export function getRealExecutionContextId(id: string | undefined | null): string | null { + if (id === NOT_INITIALIZED_CONTEXT_ID) { + return null; + } + + return id ?? null; +} diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index aa79bea519..28a2fe2fb8 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -391,7 +391,7 @@ export class ConnectionInfoResource extends CachedMapResource ): Promise> { let projectId: string | undefined; - const all = this.includes(originalKey, CachedMapAllKey); + const all = this.isAliasEqual(originalKey, CachedMapAllKey); const isProjectKey = isConnectionInfoProjectKey(originalKey); const key = this.transformParam(originalKey); @@ -418,6 +418,10 @@ export class ConnectionInfoResource extends CachedMapResource connection.id === connectionId)) { + throw new Error(`Connection is not found (${connectionId})`); + } + runInAction(() => { if (all) { this.resetIncludes(); diff --git a/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts b/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts index 920d96cd0f..94047c46c8 100644 --- a/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts +++ b/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts @@ -27,7 +27,7 @@ export interface IScheduleOptions { const queueLimit = 100; -export class TaskScheduler { +export class TaskScheduler { get activeList(): TIdentifier[] { return this.queue.map(task => task.id); } diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index be2eaec544..623ea7b6a8 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -49,6 +49,7 @@ export interface INavNodeMoveData { } export interface INavNodeRenameData { + projectId?: string; nodeId: string; newNodeId: string; } @@ -264,6 +265,7 @@ export class NavTreeResource extends CachedMapResource { this.markOutdated(node.parentId); await this.onNodeRename.execute({ + projectId: node.projectId, nodeId: node.id, newNodeId, }); diff --git a/webapp/packages/core-projects/src/extensions/IProjectProvider.ts b/webapp/packages/core-projects/src/extensions/IProjectProvider.ts new file mode 100644 index 0000000000..3bc1914340 --- /dev/null +++ b/webapp/packages/core-projects/src/extensions/IProjectProvider.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 { createExtension, isExtension, IExtension } from '@cloudbeaver/core-extensions'; + +const projectProviderSymbol = Symbol('@extension/ProjectProvider'); + +export type IProjectProvider = (context: T) => string | undefined; + +export function projectProvider(provider: IProjectProvider) { + return createExtension(provider, projectProviderSymbol); +} + +export function isProjectProvider(obj: IExtension): obj is IProjectProvider & IExtension { + return isExtension(obj, projectProviderSymbol); +} diff --git a/webapp/packages/core-projects/src/extensions/IProjectSetter.ts b/webapp/packages/core-projects/src/extensions/IProjectSetter.ts new file mode 100644 index 0000000000..b4eb81fc6a --- /dev/null +++ b/webapp/packages/core-projects/src/extensions/IProjectSetter.ts @@ -0,0 +1,25 @@ +/* + * 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 { createExtension, isExtension, IExtension } from '@cloudbeaver/core-extensions'; + + +const projectSetterSymbol = Symbol('@extension/ProjectSetter'); + +export type IProjectSetter = ( + projectId: string | null, + context: T +) => Promise | boolean; + +export function projectSetter(setter: IProjectSetter) { + return createExtension(setter, projectSetterSymbol); +} + +export function isProjectSetter(obj: IExtension): obj is IProjectSetter & IExtension { + return isExtension(obj, projectSetterSymbol); +} diff --git a/webapp/packages/core-projects/src/extensions/IProjectSetterState.ts b/webapp/packages/core-projects/src/extensions/IProjectSetterState.ts new file mode 100644 index 0000000000..3e6797fcb1 --- /dev/null +++ b/webapp/packages/core-projects/src/extensions/IProjectSetterState.ts @@ -0,0 +1,22 @@ +/* + * 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 { createExtension, isExtension, IExtension } from '@cloudbeaver/core-extensions'; + + +const projectSetterStateSymbol = Symbol('@extension/ProjectSetterState'); + +export type IProjectSetterState = (context: T) => Promise | boolean; + +export function projectSetterState(setter: IProjectSetterState) { + return createExtension(setter, projectSetterStateSymbol); +} + +export function isProjectSetterState(obj: IExtension): obj is IProjectSetterState & IExtension { + return isExtension(obj, projectSetterStateSymbol); +} diff --git a/webapp/packages/core-projects/src/index.ts b/webapp/packages/core-projects/src/index.ts index d62e9edefe..4692414c70 100644 --- a/webapp/packages/core-projects/src/index.ts +++ b/webapp/packages/core-projects/src/index.ts @@ -1,3 +1,6 @@ +export * from './extensions/IProjectProvider'; +export * from './extensions/IProjectSetter'; +export * from './extensions/IProjectSetterState'; export * from './activeProjectsContext'; export * from './ProjectInfoResource'; export * from './ProjectsService'; diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index c8e9f2af66..008893a658 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -493,14 +493,14 @@ export abstract class CachedResource< param: TParam, context: TContext, update: (param: TParam, context: TContext) => Promise, - exitCheck: () => boolean + exitCheck: (param: TParam, context: TContext) => boolean ): Promise; protected async performUpdate( param: TParam, context: TContext, update: (param: TParam, context: TContext) => Promise, - exitCheck?: () => boolean + exitCheck?: (param: TParam, context: TContext) => boolean ): Promise { const contexts = await this.beforeLoad.execute(param); @@ -510,7 +510,7 @@ export abstract class CachedResource< await this.scheduler.waitRelease(param); - if (exitCheck?.()) { + if (exitCheck?.(param, context)) { return; } @@ -519,7 +519,7 @@ export abstract class CachedResource< param, async () => { // repeated because previous task maybe has been load requested data - if (exitCheck?.()) { + if (exitCheck?.(param, context)) { return; } diff --git a/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql index 7b6061ae5f..a2091270e7 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql @@ -8,6 +8,7 @@ fragment NavNodeInfo on NavigatorNodeInfo { inline navigable features + projectId object { features } diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/getResourceList.gql b/webapp/packages/core-sdk/src/queries/resource-manager/getResourceList.gql index 34f1e27e50..5dbed1d623 100644 --- a/webapp/packages/core-sdk/src/queries/resource-manager/getResourceList.gql +++ b/webapp/packages/core-sdk/src/queries/resource-manager/getResourceList.gql @@ -1,13 +1,20 @@ -query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) { +query getResourceList( + $projectId: String! + $folder: String + $nameMask: String + $readProperties: Boolean + $readHistory: Boolean +) { resources: rmListResources( - projectId: $projectId, - folder: $folder, - nameMask: $nameMask, - readProperties: $readProperties, + projectId: $projectId + folder: $folder + nameMask: $nameMask + readProperties: $readProperties readHistory: $readHistory ) { - name, - folder, + name + folder length + properties } } \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/setResourceProperty.gql b/webapp/packages/core-sdk/src/queries/resource-manager/setResourceProperty.gql new file mode 100644 index 0000000000..3ecb2b3eb0 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/setResourceProperty.gql @@ -0,0 +1,13 @@ +mutation setResourceProperty( + $projectId: String! + $resourcePath: String! + $name: ID! + $value: String +) { + properties: rmSetResourceProperty( + projectId: $projectId + resourcePath: $resourcePath + name: $name + value: $value + ) +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 22429b1d07..8935632b87 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -413,6 +413,7 @@ export interface Mutation { rmDeleteResource?: Maybe; rmMoveResource: Scalars['String']; rmSetProjectPermissions: Scalars['Boolean']; + rmSetResourceProperty: Scalars['Boolean']; rmSetSubjectProjectPermissions: Scalars['Boolean']; rmWriteResourceStringContent: Scalars['String']; setConnectionNavigatorSettings: ConnectionInfo; @@ -612,6 +613,14 @@ export interface MutationRmSetProjectPermissionsArgs { } +export interface MutationRmSetResourcePropertyArgs { + name: Scalars['ID']; + projectId: Scalars['String']; + resourcePath: Scalars['String']; + value?: InputMaybe; +} + + export interface MutationRmSetSubjectProjectPermissionsArgs { permissions: Array; subjectId: Scalars['String']; @@ -1379,6 +1388,7 @@ export interface RmResource { folder: Scalars['Boolean']; length: Scalars['Int']; name: Scalars['String']; + properties?: Maybe; } export interface RmSubjectProjectPermissions { @@ -2256,7 +2266,7 @@ export type NavGetStructContainersQueryVariables = Exact<{ }>; -export type NavGetStructContainersQuery = { navGetStructContainers: { supportsCatalogChange: boolean, supportsSchemaChange: boolean, catalogList: Array<{ catalog: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> }>, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> } }; +export type NavGetStructContainersQuery = { navGetStructContainers: { supportsCatalogChange: boolean, supportsSchemaChange: boolean, catalogList: Array<{ catalog: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> }>, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> } }; export type AdminObjectGrantInfoFragment = { subjectId: string, subjectType: AdminSubjectType, objectPermissions: { objectId: string, permissions: Array } }; @@ -2288,7 +2298,7 @@ export type ExecutionContextInfoFragment = { id: string, projectId: string, conn export type NavNodeDbObjectInfoFragment = { id: string, object?: { type?: string, features?: Array, properties?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> } }; -export type NavNodeInfoFragment = { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }; +export type NavNodeInfoFragment = { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }; export type NavNodePropertiesFragment = { id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }; @@ -2449,7 +2459,7 @@ export type GetNodeParentsQueryVariables = Exact<{ }>; -export type GetNodeParentsQuery = { node: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> }; +export type GetNodeParentsQuery = { node: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> }; export type NavDeleteNodesMutationVariables = Exact<{ nodePaths: Array | Scalars['ID']; @@ -2474,7 +2484,7 @@ export type NavNodeChildrenQueryVariables = Exact<{ }>; -export type NavNodeChildrenQuery = { navNodeChildren: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }>, navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> } }; +export type NavNodeChildrenQuery = { navNodeChildren: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }>, navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> } }; export type NavNodeInfoQueryVariables = Exact<{ nodePath: Scalars['ID']; @@ -2482,7 +2492,7 @@ export type NavNodeInfoQueryVariables = Exact<{ }>; -export type NavNodeInfoQuery = { navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> } }; +export type NavNodeInfoQuery = { navNodeInfo: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, projectId?: string, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> } }; export type NavRefreshNodeQueryVariables = Exact<{ nodePath: Scalars['ID']; @@ -2566,7 +2576,7 @@ export type GetResourceListQueryVariables = Exact<{ }>; -export type GetResourceListQuery = { resources: Array<{ name: string, folder: boolean, length: number }> }; +export type GetResourceListQuery = { resources: Array<{ name: string, folder: boolean, length: number, properties?: any }> }; export type GetResourceProjectListQueryVariables = Exact<{ [key: string]: never; }>; @@ -2610,6 +2620,16 @@ export type SetProjectPermissionsMutationVariables = Exact<{ export type SetProjectPermissionsMutation = { rmSetProjectPermissions: boolean }; +export type SetResourcePropertyMutationVariables = Exact<{ + projectId: Scalars['String']; + resourcePath: Scalars['String']; + name: Scalars['ID']; + value?: InputMaybe; +}>; + + +export type SetResourcePropertyMutation = { properties: boolean }; + export type SetSubjectProjectsPermissionsMutationVariables = Exact<{ subjectId: Scalars['String']; permissions: Array | RmProjectPermissions; @@ -3074,6 +3094,7 @@ export const NavNodeInfoFragmentDoc = ` inline navigable features + projectId object { features } @@ -4146,6 +4167,7 @@ export const GetResourceListDocument = ` name folder length + properties } } `; @@ -4196,6 +4218,16 @@ export const SetProjectPermissionsDocument = ` rmSetProjectPermissions(projectId: $projectId, permissions: $permissions) } `; +export const SetResourcePropertyDocument = ` + mutation setResourceProperty($projectId: String!, $resourcePath: String!, $name: ID!, $value: String) { + properties: rmSetResourceProperty( + projectId: $projectId + resourcePath: $resourcePath + name: $name + value: $value + ) +} + `; export const SetSubjectProjectsPermissionsDocument = ` mutation setSubjectProjectsPermissions($subjectId: String!, $permissions: [RMProjectPermissions!]!) { rmSetSubjectProjectPermissions(subjectId: $subjectId, permissions: $permissions) @@ -4747,6 +4779,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = setProjectPermissions(variables: SetProjectPermissionsMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(SetProjectPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setProjectPermissions', 'mutation'); }, + setResourceProperty(variables: SetResourcePropertyMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(SetResourcePropertyDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setResourceProperty', 'mutation'); + }, setSubjectProjectsPermissions(variables: SetSubjectProjectsPermissionsMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(SetSubjectProjectsPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setSubjectProjectsPermissions', 'mutation'); }, diff --git a/webapp/packages/core-utils/src/isValuesEqual.ts b/webapp/packages/core-utils/src/isValuesEqual.ts index 598d275d8c..acd62eff28 100644 --- a/webapp/packages/core-utils/src/isValuesEqual.ts +++ b/webapp/packages/core-utils/src/isValuesEqual.ts @@ -9,7 +9,7 @@ export function isValuesEqual( first: T | null | undefined, second: T | null | undefined, - defaultValue?: T + defaultValue?: T | null | undefined ): boolean { return (first ?? defaultValue) === (second ?? defaultValue); } diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts index 7a4c62b5d8..af43da44dc 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts @@ -59,7 +59,17 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { items = [...items]; const connections = this.connectionsManagerService.projectConnections - .filter(connection => !connection.template) + .filter(connection => { + if ( + !this.connectionSchemaManagerService.isProjectChangeable + && this.connectionSchemaManagerService.activeProjectId + && connection.projectId !== this.connectionSchemaManagerService.activeProjectId + ) { + return false; + } + + return !connection.template; + }) .sort((a, b) => { if (a.connected === b.connected) { return compareConnectionsInfo(a, b); diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index cdbcab16c1..a5aed98793 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -13,6 +13,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExtensionUtils, IExtension } from '@cloudbeaver/core-extensions'; import { type IObjectNavNodeProvider, type IDataContextActiveNode, isObjectNavNodeProvider } from '@cloudbeaver/core-navigation-tree'; +import { IProjectProvider, IProjectSetter, IProjectSetterState, isProjectProvider, isProjectSetter, isProjectSetterState } from '@cloudbeaver/core-projects'; import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; @@ -25,10 +26,13 @@ interface IActiveItem { id: string; context: T; getCurrentNavNode?: IObjectNavNodeProvider; + getProjectSetterState?: IProjectSetterState; + getCurrentProjectId?: IProjectProvider; getCurrentConnectionId?: IConnectionProvider; getCurrentSchemaId?: IObjectSchemaProvider; getCurrentCatalogId?: IObjectCatalogProvider; changeConnectionId?: IConnectionSetter; + changeProjectId?: IProjectSetter; changeCatalogId?: IObjectCatalogSetter; changeSchemaId?: IObjectSchemaSetter; } @@ -44,6 +48,15 @@ export class ConnectionSchemaManagerService { return this.activeItem.getCurrentNavNode(this.activeItem.context); } + get activeProjectId(): string | null | undefined { + + if (!this.activeItem?.getCurrentProjectId) { + return null; + } + + return this.activeItem.getCurrentProjectId(this.activeItem.context); + } + get activeConnectionKey(): IConnectionInfoParams | null | undefined { if (!this.activeItem?.getCurrentConnectionId) { @@ -53,6 +66,14 @@ export class ConnectionSchemaManagerService { return this.activeItem.getCurrentConnectionId(this.activeItem.context); } + get currentProjectId(): string | null | undefined { + if (this.pendingProjectId !== null) { + return this.pendingProjectId; + } + + return this.activeProjectId; + } + get currentConnectionKey(): IConnectionInfoParams | null | undefined { if (this.pendingConnectionKey !== null) { return this.pendingConnectionKey; @@ -130,6 +151,10 @@ export class ConnectionSchemaManagerService { }); } + get isProjectChangeable(): boolean { + return !!this.activeItem?.changeProjectId && !!this.activeItem.getProjectSetterState?.(this.activeItem.context); + } + get isConnectionChangeable(): boolean { return !!this.activeItem?.changeConnectionId; } @@ -148,6 +173,10 @@ export class ConnectionSchemaManagerService { ); } + get isChangingProject(): boolean { + return this.changingProjectId; + } + get isChangingConnection(): boolean { return this.changingConnection; } @@ -177,9 +206,11 @@ export class ConnectionSchemaManagerService { return item; } + private pendingProjectId: string | null; private pendingConnectionKey: IConnectionInfoParams | null; private pendingCatalogId: string | null | undefined; private pendingSchemaId: string | null | undefined; + private changingProjectId: boolean; private changingConnection: boolean; private changingConnectionContainer: boolean; @@ -190,17 +221,21 @@ export class ConnectionSchemaManagerService { private readonly dbDriverResource: DBDriverResource, private readonly notificationService: NotificationService ) { + this.changingProjectId = false; this.changingConnection = false; this.changingConnectionContainer = false; this.pendingConnectionKey = null; + this.pendingProjectId = null; this.pendingCatalogId = null; this.pendingSchemaId = null; makeObservable< ConnectionSchemaManagerService, 'activeItem' + | 'changingProjectId' | 'changingConnection' | 'changingConnectionContainer' + | 'pendingProjectId' | 'pendingConnectionKey' | 'pendingCatalogId' | 'pendingSchemaId' @@ -218,8 +253,10 @@ export class ConnectionSchemaManagerService { isObjectCatalogChangeable: computed, isObjectSchemaChangeable: computed, activeItem: computed, + changingProjectId: observable, changingConnection: observable, changingConnectionContainer: observable, + pendingProjectId: observable, pendingConnectionKey: observable, pendingCatalogId: observable, pendingSchemaId: observable, @@ -227,6 +264,24 @@ export class ConnectionSchemaManagerService { }); } + async selectProjectId(projectId: string): Promise { + if (!this.activeItem?.changeProjectId) { + return; + } + try { + runInAction(() => { + this.changingProjectId = true; + this.pendingProjectId = projectId; + }); + await this.activeItem.changeProjectId(projectId, this.activeItem.context); + } finally { + runInAction(() => { + this.changingProjectId = false; + this.pendingProjectId = null; + }); + } + } + /** * Trigger when user select connection in dropdown */ @@ -236,7 +291,9 @@ export class ConnectionSchemaManagerService { } try { runInAction(() => { + this.changingProjectId = true; this.changingConnection = true; + this.pendingProjectId = connectionKey.projectId; this.pendingConnectionKey = connectionKey; this.pendingSchemaId = undefined; this.pendingCatalogId = undefined; @@ -245,7 +302,9 @@ export class ConnectionSchemaManagerService { await this.updateContainer(connectionKey); } finally { runInAction(() => { + this.changingProjectId = false; this.changingConnection = false; + this.pendingProjectId = null; this.pendingConnectionKey = null; this.pendingSchemaId = null; this.pendingCatalogId = null; @@ -361,10 +420,13 @@ export class ConnectionSchemaManagerService { private setExtensions(item: IActiveItem, extensions: Array>) { ExtensionUtils.from(extensions) .on(isObjectNavNodeProvider, extension => { item.getCurrentNavNode = extension; }) + .on(isProjectSetterState, extension => { item.getProjectSetterState = extension; }) + .on(isProjectProvider, extension => { item.getCurrentProjectId = extension; }) .on(isConnectionProvider, extension => { item.getCurrentConnectionId = extension; }) .on(isObjectCatalogProvider, extension => { item.getCurrentCatalogId = extension; }) .on(isObjectSchemaProvider, extension => { item.getCurrentSchemaId = extension; }) + .on(isProjectSetter, extension => { item.changeProjectId = extension; }) .on(isConnectionSetter, extension => { item.changeConnectionId = extension; }) .on(isObjectCatalogSetter, extension => { item.changeCatalogId = extension; }) .on(isObjectSchemaSetter, extension => { item.changeSchemaId = extension; }); diff --git a/webapp/packages/plugin-projects/src/ProjectSelect.tsx b/webapp/packages/plugin-projects/src/ProjectSelect.tsx index 579ebebfef..959dbb2cc0 100644 --- a/webapp/packages/plugin-projects/src/ProjectSelect.tsx +++ b/webapp/packages/plugin-projects/src/ProjectSelect.tsx @@ -45,7 +45,10 @@ export const ProjectSelect = observer(function ProjectSelect({ const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, { onData: () => { - if (!value && possibleOptions.length > 0) { + if ( + (!value && possibleOptions.length > 0) + || (value && !possibleOptions.includes(value)) + ) { onChange(possibleOptions[0]); } }, diff --git a/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts b/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts index 772c8d5026..45dd8d18a5 100644 --- a/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts +++ b/webapp/packages/plugin-resource-manager/src/NavResourceNodeService.ts @@ -65,6 +65,23 @@ export class NavResourceNodeService { ); } + async setProperties(resourceData: IResourceData, diff: Record): Promise> { + return await this.resourceManagerResource.setProperties( + resourceData.key.projectId, + resourceData.resourcePath, + diff + ); + } + + async getProperties(resourceData: IResourceData): Promise> { + const resource = await this.resourceManagerResource.loadProperties( + resourceData.key.projectId, + resourceData.resourcePath + ); + + return resource; + } + getResourceData(nodeId: string): IResourceData | undefined { if (!isRMNavNode(nodeId)) { return; diff --git a/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts b/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts index dd6002397b..3415d25b48 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceManagerResource.ts @@ -8,6 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedMapResource, GraphQLService, ResourceKey, ResourceKeyUtils, RmResource } from '@cloudbeaver/core-sdk'; +import { isValuesEqual } from '@cloudbeaver/core-utils'; export type RmResourceInfo = RmResource; export interface IResourceManagerParams { @@ -23,6 +24,11 @@ export class ResourceManagerResource extends CachedMapResource resource.name === resourcePath) === true; + return resources?.some(resource => resource.name === name) === true; } getResource(key: IResourceManagerParams, resourcePath: string): RmResourceInfo | undefined { const resources = this.get(key); + const name = this.getResourceName(resourcePath); - return resources?.find(resource => resource.name === resourcePath); + return resources?.find(resource => resource.name === name); } async move(projectId: string, oldPath: string, newPath: string): Promise { @@ -55,6 +63,81 @@ export class ResourceManagerResource extends CachedMapResource> { + const folder = this.getFolder(resourcePath); + const nameMask = this.getResourceName(resourcePath); + const key: IResourceManagerParams = { projectId, folder }; + + await this.load(key); + await this.performUpdate(key, undefined, async () => { + const { resources } = await this.graphQLService.sdk.getResourceList({ + projectId, + folder, + nameMask, + readProperties: true, + }); + + const currentResources = this.get(key); + const resource = this.getResource(key, resourcePath); + + if (resource) { + currentResources?.splice( + currentResources.indexOf(resource), + 1, + ...resources.map(resource => ({ properties: {}, ...resource })) + ); + } + + // this.dataSet( + // key, + // resources.map(resource => ({ properties: {}, ...resource })) + // ); + }, () => { + const resource = this.getResource(key, resourcePath); + return resource?.properties && this.isLoaded(key) && !this.isOutdated(key); + }); + + return this.getResource(key, resourcePath)?.properties ?? {}; + } + + async setProperties( + projectId: string, + resourcePath: string, + diff: Record + ): Promise> { + const folder = this.getFolder(resourcePath); + const key: IResourceManagerParams = { projectId, folder }; + const propertiesPatch: Record = {}; + const properties = await this.loadProperties(projectId, resourcePath); + + await this.performUpdate(key, undefined, async () => { + for (const [name, value] of Object.entries(diff)) { + if ( + properties[name] === value + || (value === null && !(name in properties)) + ) { + continue; + } + + await this.graphQLService.sdk.setResourceProperty({ + projectId, + resourcePath, + name, + value, + }); + + propertiesPatch[name] = value; + } + + Object.assign(properties, propertiesPatch); + }); + + return properties; + } + async createResource(projectId: string, resourcePath: string, folder: boolean) { await this.graphQLService.sdk.createResource({ projectId, @@ -62,7 +145,7 @@ export class ResourceManagerResource extends CachedMapResource k.projectId === key.projectId && k.folder === key.folder); + const ref = this.keys.find(k => k.projectId === key.projectId && isValuesEqual(k.folder, key.folder, '')); if (ref) { return ref; @@ -117,7 +200,7 @@ export class ResourceManagerResource extends CachedMapResource resource)); }); return this.data; diff --git a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx index 7404b3b4e3..74e03dd705 100644 --- a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx +++ b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx @@ -22,6 +22,7 @@ const style = css` interface Payload { defaultScriptName?: string; + projectId?: string | null; } export interface ISaveScriptDialogResult { @@ -48,7 +49,7 @@ export const SaveScriptDialog: DialogComponent const state = useObservableRef(() => ({ name: payload.defaultScriptName ?? '', - projectId: null, + projectId: payload.projectId ?? null, errorMessage: null, validate() { this.errorMessage = null; @@ -116,7 +117,7 @@ export const SaveScriptDialog: DialogComponent p.canEditResources} + filter={p => (p.canEditResources && p.id === (payload.projectId ?? p.id))} autoHide onChange={projectId => {state.projectId = projectId;}} /> diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/IResourceSqlDataSourceState.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/IResourceSqlDataSourceState.ts index 766de24d17..6dd1bfde90 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/IResourceSqlDataSourceState.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/IResourceSqlDataSourceState.ts @@ -10,6 +10,7 @@ import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connecti export interface IResourceNodeInfo { nodeId: string; + projectId?: string; parents: string[]; } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts index e34f2554f5..0d37cdafde 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts @@ -11,13 +11,14 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial import { NotificationService } from '@cloudbeaver/core-events'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { NavNodeManagerService, NavTreeResource, NavNodeInfoResource, NodeManagerUtils, type INodeNavigationData, NavigationType } from '@cloudbeaver/core-navigation-tree'; -import { ProjectsService } from '@cloudbeaver/core-projects'; +import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { DATA_CONTEXT_TAB_ID } from '@cloudbeaver/core-ui'; import { createPath } from '@cloudbeaver/core-utils'; import { ActionService, ACTION_SAVE, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; -import { NavResourceNodeService, RESOURCE_NODE_TYPE, SaveScriptDialog, ResourceManagerService, RESOURCES_NODE_PATH, ResourceProjectsResource, ResourcesProjectsNavNodeService } from '@cloudbeaver/plugin-resource-manager'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, SqlDataSourceService, SqlEditorService, SqlEditorSettingsService, SQL_EDITOR_ACTIONS_MENU } from '@cloudbeaver/plugin-sql-editor'; +import { NavResourceNodeService, RESOURCE_NODE_TYPE, SaveScriptDialog, ResourceManagerService, RESOURCES_NODE_PATH, ResourcesProjectsNavNodeService } from '@cloudbeaver/plugin-resource-manager'; +import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, SqlDataSourceService, SqlEditorSettingsService, SQL_EDITOR_ACTIONS_MENU } from '@cloudbeaver/plugin-sql-editor'; import { isSQLEditorTab, SqlEditorNavigatorService, SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; import { isScript } from './isScript'; @@ -31,14 +32,13 @@ export class PluginBootstrap extends Bootstrap { private readonly navNodeManagerService: NavNodeManagerService, private readonly navTreeResource: NavTreeResource, private readonly navResourceNodeService: NavResourceNodeService, - private readonly sqlEditorService: SqlEditorService, private readonly navNodeInfoResource: NavNodeInfoResource, private readonly navigationTabsService: NavigationTabsService, private readonly notificationService: NotificationService, private readonly sqlEditorNavigatorService: SqlEditorNavigatorService, private readonly resourceManagerService: ResourceManagerService, private readonly projectsService: ProjectsService, - private readonly resourceProjectsResource: ResourceProjectsResource, + private readonly projectInfoResource: ProjectInfoResource, private readonly sqlEditorTabResourceService: SqlEditorTabResourceService, private readonly commonDialogService: CommonDialogService, private readonly actionService: ActionService, @@ -86,20 +86,33 @@ export class PluginBootstrap extends Bootstrap { } if (action === ACTION_SAVE) { + let projectId = dataSource.executionContext?.projectId ?? null; + await this.projectInfoResource.load(CachedMapAllKey); const name = getSqlEditorName(state, dataSource); + + if (projectId) { + const project = this.projectInfoResource.get(projectId); + + if (!project?.canEditResources) { + projectId = null; + } + } + const result = await this.commonDialogService.open(SaveScriptDialog, { defaultScriptName: name, + projectId, }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { try { - if (!result.projectId) { + projectId = result.projectId; + + if (!projectId) { throw new Error('Project not selected'); } - await this.resourceProjectsResource.load(); const scriptName = `${result.name.trim()}.${SCRIPT_EXTENSION}`; - const folder = createPath(RESOURCES_NODE_PATH, result.projectId); + const folder = createPath(RESOURCES_NODE_PATH, projectId); const resourceData = this.navResourceNodeService.getResourceData(folder); if (!resourceData) { @@ -109,7 +122,6 @@ export class PluginBootstrap extends Bootstrap { const nodeId = await this.navResourceNodeService.saveScript(resourceData, scriptName, dataSource.script); - await this.navTreeResource.preloadNodeParents(NodeManagerUtils.parentsFromPath(nodeId), nodeId); const node = await this.navNodeInfoResource.load(nodeId); @@ -124,16 +136,16 @@ export class PluginBootstrap extends Bootstrap { const parents = NodeManagerUtils.parentsFromPath(nodeId); dataSource.setNodeInfo({ + projectId, nodeId, parents, }); - if (previousDataSource) { + if (previousDataSource?.executionContext) { dataSource.setExecutionContext(previousDataSource.executionContext); } } - this.sqlEditorService.setName(node.name ?? scriptName, state); this.notificationService.logSuccess({ title: 'plugin_resource_manager_save_script_success', message: node.name }); if (!this.resourceManagerService.active) { @@ -178,7 +190,7 @@ export class PluginBootstrap extends Bootstrap { } try { - const nodeInfo = await contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); + const nodeInfo = contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); const node = await this.navNodeInfoResource.load(data.nodeId); if (node.nodeType !== RESOURCE_NODE_TYPE || !isScript(node.id)) { @@ -246,11 +258,12 @@ export class PluginBootstrap extends Bootstrap { const parents = NodeManagerUtils.parentsFromPath(nodeId); dataSource.setNodeInfo({ + projectId: node.projectId, nodeId, parents, }); - if (previousDataSource) { + if (previousDataSource?.executionContext) { dataSource.setExecutionContext(previousDataSource.executionContext); } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts index 7d31cbdea6..14797f1b1f 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSource.ts @@ -6,15 +6,22 @@ * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction, toJS } from 'mobx'; -import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; +import { IConnectionExecutionContextInfo, NOT_INITIALIZED_CONTEXT_ID } from '@cloudbeaver/core-connections'; +import { TaskScheduler } from '@cloudbeaver/core-executor'; import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; -import { debounce, isArraysEqual } from '@cloudbeaver/core-utils'; +import { debounce, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; import { BaseSqlDataSource, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-sql-editor'; import type { IResourceNodeInfo, IResourceSqlDataSourceState } from './IResourceSqlDataSourceState'; +interface IResourceProperties { + 'default-datasource'?: string; + 'default-catalog'?: string; + 'default-schema'?: string; +} + interface IResourceInfo { isReadonly?: (dataSource: ResourceSqlDataSource) => boolean; } @@ -23,6 +30,15 @@ interface IResourceActions { rename(dataSource: ResourceSqlDataSource, nodeId: string, name: string): Promise; read(dataSource: ResourceSqlDataSource, nodeId: string): Promise; write(dataSource: ResourceSqlDataSource, nodeId: string, value: string): Promise; + getProperties( + dataSource: ResourceSqlDataSource, + nodeId: string + ): Promise>; + setProperties( + dataSource: ResourceSqlDataSource, + nodeId: string, + diff: Record + ): Promise>; } const VALUE_SYNC_DELAY = 1 * 1000; @@ -42,6 +58,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { return this._script; } + get projectId(): string | null { + return this.nodeInfo?.projectId ?? null; + } + get executionContext(): IConnectionExecutionContextInfo | undefined { return this.state.executionContext; } @@ -68,9 +88,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { private info?: IResourceInfo; private lastAction?: () => Promise; private readonly state: IResourceSqlDataSourceState; + private resourceProperties: IResourceProperties; - private loading: boolean; private loaded: boolean; + private readonly scheduler: TaskScheduler; constructor( private readonly navNodeInfoResource: NavNodeInfoResource, @@ -80,11 +101,12 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.state = state; this._script = ''; this.saved = true; - this.loading = false; this.loaded = false; + this.resourceProperties = {}; + this.scheduler = new TaskScheduler(() => true); this.debouncedWrite = debounce(this.debouncedWrite.bind(this), VALUE_SYNC_DELAY); - makeObservable(this, { + makeObservable(this, { script: computed, executionContext: computed, nodeInfo: computed, @@ -92,9 +114,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { equals: isArraysEqual, }), _script: observable, - lastAction: observable, - loading: observable, + lastAction: observable.ref, loaded: observable, + resourceProperties: observable, + setExecutionContext: action, setScript: action, setNodeInfo: action, }); @@ -113,7 +136,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { } isLoading(): boolean { - return this.loading; + return this.scheduler.executing; } setNodeInfo(nodeInfo?: IResourceNodeInfo): void { @@ -135,6 +158,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.rename(name); } + setProject(projectId: string | null): void { + + } + canRename(name: string | null): boolean { if (this.isReadonly()) { return false; @@ -168,96 +195,205 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { async load(): Promise { await this.read(); + await this.updateProperties(); } setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void { - this.state.executionContext = executionContext; + if ( + this.nodeInfo?.projectId + && executionContext?.projectId + && this.nodeInfo.projectId !== executionContext.projectId + ) { + console.warn('Cant change execution context because of different projects'); + return; + } + + if ( + !isObjectsEqual(toJS(this.state.executionContext), toJS(executionContext)) + ) { + this.state.executionContext = toJS(executionContext); + } + + if ( + this.isReadonly() + || ( + this.resourceProperties['default-datasource'] === executionContext?.connectionId + && this.resourceProperties['default-catalog'] === executionContext?.defaultCatalog + && this.resourceProperties['default-schema'] === executionContext?.defaultSchema + ) + ) { + return; + } + + this.resourceProperties['default-datasource'] = executionContext?.connectionId; + this.resourceProperties['default-catalog'] = executionContext?.defaultCatalog; + this.resourceProperties['default-schema'] = executionContext?.defaultSchema; + + this.setProperties(toJS(this.resourceProperties)); } async rename(name: string | null) { await this.write(); - if ( - !this.actions + await this.scheduler.schedule(undefined, async () => { + if ( + !this.actions || !this.nodeInfo || !this.saved || !name?.trim() - ) { - return; - } - - if (!name.toLowerCase().endsWith('.sql')) { - name += '.sql'; - } - - this.lastAction = this.rename.bind(this, name); - this.loading = true; - this.message = 'Renaming script...'; - - try { - this.exception = null; - this.nodeInfo.nodeId = await this.actions.rename(this, this.nodeInfo.nodeId, name); - - if (this.nodeInfo.parents.length > 0) { - this.nodeInfo.parents.splice(this.nodeInfo.parents.length - 1, 1, this.nodeInfo.nodeId); + ) { + return; } - this.markOutdated(); - this.saved = true; - this.loaded = false; - } catch (exception: any) { - this.exception = exception; - } finally { - this.loading = false; - this.message = undefined; - } + if (!name.toLowerCase().endsWith('.sql')) { + name += '.sql'; + } + + this.lastAction = this.rename.bind(this, name); + this.message = 'Renaming script...'; + + try { + this.exception = null; + this.nodeInfo.nodeId = await this.actions.rename(this, this.nodeInfo.nodeId, name); + + if (this.nodeInfo.parents.length > 0) { + this.nodeInfo.parents.splice(this.nodeInfo.parents.length - 1, 1, this.nodeInfo.nodeId); + } + + this.markOutdated(); + this.saved = true; + this.loaded = false; + } catch (exception: any) { + this.exception = exception; + } finally { + this.message = undefined; + } + }); } async read() { - if (!this.actions || !this.nodeInfo || !this.isOutdated() || !this.saved) { - return; - } + await this.scheduler.schedule(undefined, async () => { + if (!this.actions || !this.nodeInfo || !this.isOutdated() || !this.saved) { + return; + } - this.lastAction = this.read.bind(this); - this.loading = true; - this.message = 'Reading script...'; + this.lastAction = this.read.bind(this); + this.message = 'Reading script...'; - try { - this.exception = null; - this._script = await this.actions.read(this, this.nodeInfo.nodeId); - this.state.name = this.name ?? undefined; - this.markUpdated(); - this.loaded = true; - super.setScript(this.script); - } catch (exception: any) { - this.exception = exception; - } finally { - this.loading = false; - this.message = undefined; - } + try { + this.exception = null; + this._script = await this.actions.read(this, this.nodeInfo.nodeId); + this.state.name = this.name ?? undefined; + this.markUpdated(); + this.loaded = true; + super.setScript(this.script); + } catch (exception: any) { + this.exception = exception; + } finally { + this.message = undefined; + } + }); } async write() { - if (!this.actions || !this.nodeInfo || this.saved) { + await this.scheduler.schedule(undefined, async () => { + if (!this.actions || !this.nodeInfo || this.saved) { + return; + } + + this.lastAction = this.write.bind(this); + this.message = 'Saving script...'; + + try { + this.exception = null; + await this.actions.write(this, this.nodeInfo.nodeId, this.script); + this.state.name = this.name ?? undefined; + this.saved = true; + this.markUpdated(); + } catch (exception: any) { + this.exception = exception; + } finally { + this.message = undefined; + } + }); + } + + async setProperties(properties: IResourceProperties) { + if (Object.keys(properties).length === 0) { return; } - this.lastAction = this.write.bind(this); - this.loading = true; - this.message = 'Saving script...'; + await this.scheduler.schedule(undefined, async () => { + if (!this.actions || !this.nodeInfo) { + return; + } - try { - this.exception = null; - await this.actions.write(this, this.nodeInfo.nodeId, this.script); - this.state.name = this.name ?? undefined; - this.saved = true; - this.markUpdated(); - } catch (exception: any) { - this.exception = exception; - } finally { - this.loading = false; - this.message = undefined; + this.lastAction = this.setProperties.bind(this, properties); + this.message = 'Update info...'; + + try { + this.exception = null; + + if (!this.isReadonly()) { + await this.actions.setProperties( + this, + this.nodeInfo.nodeId, + properties + ); + } + + await this.updateProperties(); + } catch (exception: any) { + this.exception = exception; + } finally { + this.message = undefined; + } + }); + } + + private async updateProperties() { + if (!this.actions || !this.nodeInfo) { + return; } + + const previousProperties = this.resourceProperties; + + const resourceProperties = await this.actions.getProperties(this, this.nodeInfo.nodeId); + + runInAction(() => { + this.resourceProperties = toJS(resourceProperties); + + if (isObjectsEqual(toJS(previousProperties), toJS(this.resourceProperties)) && this.isReadonly()) { + return; + } + + const connectionId = this.resourceProperties['default-datasource']; + const defaultCatalog = this.resourceProperties['default-catalog']; + const defaultSchema = this.resourceProperties['default-schema']; + + if (!this.nodeInfo!.projectId) { + return; + } + + if (connectionId) { + if ( + !isValuesEqual(this.state.executionContext?.connectionId, connectionId, null) + || !isValuesEqual(this.state.executionContext?.defaultCatalog, defaultCatalog, null) + || !isValuesEqual(this.state.executionContext?.defaultSchema, defaultSchema, null) + || !isValuesEqual(this.state.executionContext?.projectId, this.nodeInfo!.projectId, null) + ) { + this.state.executionContext = { + id: NOT_INITIALIZED_CONTEXT_ID, + projectId: this.nodeInfo!.projectId, + connectionId, + defaultCatalog, + defaultSchema, + }; + } + } else { + this.state.executionContext = undefined; + } + }); } private debouncedWrite() { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts index 24d6094809..39fc5f229b 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/ResourceSqlDataSourceBootstrap.ts @@ -8,7 +8,6 @@ import { action, makeObservable, observable, untracked } from 'mobx'; -import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -69,6 +68,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { !['undefined', 'object'].includes(typeof value.nodeInfo) || !['string', 'undefined'].includes(typeof value.name) || !['string', 'undefined'].includes(typeof value.nodeInfo?.nodeId) + || !['string', 'undefined'].includes(typeof value.nodeInfo?.projectId) || !['undefined', 'object'].includes(typeof value.nodeInfo?.parents) || !['undefined', 'object'].includes(typeof value.executionContext) || !['string', 'undefined'].includes(typeof value.executionContext?.connectionId) @@ -97,10 +97,13 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { this.navNodeInfoResource, this.createState( editorId, - options?.executionContext, ) ); + if (options?.executionContext) { + dataSource.setExecutionContext(options.executionContext); + } + if (options?.script) { dataSource.setScript(options.script); } @@ -113,6 +116,8 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { rename: this.rename.bind(this), read: this.read.bind(this), write: this.write.bind(this), + getProperties: this.getProperties.bind(this), + setProperties: this.setProperties.bind(this), }); dataSource.setInfo({ @@ -169,14 +174,12 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { private createState( editorId: string, - executionContext?: IConnectionExecutionContextInfo, nodeInfo?: IResourceNodeInfo ): IResourceSqlDataSourceState { let state = this.dataSourceStateState.get(editorId); if (!state) { state = observable({ - executionContext, nodeInfo, }); @@ -309,6 +312,53 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { } } + private async getProperties( + dataSource: ResourceSqlDataSource, + nodeId: string + ): Promise> { + if (!dataSource.nodeInfo) { + throw new Error('Node info is not provided'); + } + + try { + await this.navTreeResource.preloadNodeParents(dataSource.nodeInfo.parents, dataSource.nodeInfo.nodeId); + const resourceData = this.navResourceNodeService.getResourceData(nodeId); + + if (!resourceData) { + throw new Error('Can\'t find resource'); + } + + return await this.navResourceNodeService.getProperties(resourceData); + } catch (exception) { + this.notificationService.logException(exception as any, 'plugin_resource_manager_sync_script_error'); + throw exception; + } + } + + private async setProperties( + dataSource: ResourceSqlDataSource, + nodeId: string, + diff: Record + ): Promise> { + if (!this.resourceManagerService.enabled || !dataSource.nodeInfo) { + return {}; + } + + try { + await this.navTreeResource.preloadNodeParents(dataSource.nodeInfo.parents, dataSource.nodeInfo.nodeId); + const resourceData = this.navResourceNodeService.getResourceData(nodeId); + + if (!resourceData) { + return {}; + } + + return await this.navResourceNodeService.setProperties(resourceData, diff); + } catch (exception) { + this.notificationService.logException(exception as any, 'plugin_resource_manager_update_script_error'); + throw exception; + } + } + private async read(dataSource: ResourceSqlDataSource, nodeId: string): Promise { if (!dataSource.nodeInfo) { throw new Error('Node info is not provided'); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index 8a269624fa..9b82552c10 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -29,9 +29,10 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { objectNavNodeProvider, NodeManagerUtils, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import { projectProvider, projectSetter, projectSetterState } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, NavNodeInfoFragment, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { NavigationTabsService, TabHandler, ITab, ITabOptions } from '@cloudbeaver/plugin-navigation-tabs'; -import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; +import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, SqlDataSourceService, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-sql-editor'; import { isSQLEditorTab } from './isSQLEditorTab'; import { SqlEditorPanel } from './SqlEditorPanel'; @@ -75,9 +76,12 @@ export class SqlEditorTabService extends Bootstrap { canClose: this.handleCanTabClose.bind(this), extensions: [ objectNavNodeProvider(this.getNavNode.bind(this)), + projectSetterState(this.getProjectSetState.bind(this)), + projectProvider(this.getProjectId.bind(this)), connectionProvider(this.getConnectionId.bind(this)), objectCatalogProvider(this.getObjectCatalogId.bind(this)), objectSchemaProvider(this.getObjectSchemaId.bind(this)), + projectSetter(this.setProjectId.bind(this)), connectionSetter((connectionId, tab) => this.setConnectionId(tab, connectionId)), objectCatalogSetter(this.setObjectCatalogId.bind(this)), objectSchemaSetter(this.setObjectSchemaId.bind(this)), @@ -211,7 +215,10 @@ export class SqlEditorTabService extends Bootstrap { isSQLEditorTab(tab => { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); - return !!dataSource?.executionContext; + return ( + !!dataSource?.executionContext + && ResourceKeyUtils.includes(key, dataSource.executionContext.id) + ); }) ); @@ -318,6 +325,18 @@ export class SqlEditorTabService extends Bootstrap { return true; } + private getProjectId(tab: ITab): string | undefined { + const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); + + return dataSource?.projectId ?? undefined; + } + + private getProjectSetState(tab: ITab): boolean { + const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); + + return !!dataSource?.features.includes(ESqlDataSourceFeatures.setProject); + } + private getConnectionId(tab: ITab): IConnectionInfoParams | undefined { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); @@ -343,6 +362,15 @@ export class SqlEditorTabService extends Bootstrap { return context?.defaultSchema; } + private setProjectId(projectId: string | null, tab: ITab): boolean { + const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); + + dataSource?.setProject(projectId); + this.attachToProject(tab, projectId); + + return true; + } + async setConnectionId( tab: ITab, connectionKey: IConnectionInfoParams, diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts index 470b16297f..89d8905da3 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts @@ -20,6 +20,11 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { abstract get executionContext(): IConnectionExecutionContextInfo | undefined; exception?: Error | Error[] | null | undefined; message?: string; + + get projectId(): string | null { + return null; + } + get features(): ESqlDataSourceFeatures[] { return []; } @@ -91,6 +96,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.editing = state; } + setProject(projectId: string | null): void { + + } + load(): Promise | void { } dispose(): void | Promise { } } \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts index c7c1cc355f..ba5a4ee3a7 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts @@ -7,5 +7,6 @@ */ export enum ESqlDataSourceFeatures { - setName + setName, + setProject } \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts index 0446b9bfcf..4541c948bc 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts @@ -15,6 +15,7 @@ import type { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures'; export interface ISqlDataSource extends ILoadableState { readonly name: string | null; readonly script: string; + readonly projectId: string | null; readonly executionContext?: IConnectionExecutionContextInfo; readonly message?: string; readonly onSetScript: ISyncExecutor; @@ -27,6 +28,7 @@ export interface ISqlDataSource extends ILoadableState { markUpdated(): void; canRename(name: string | null): boolean; setName(name: string | null): void; + setProject(projectId: string | null): void; setScript(script: string): void; setEditing(state: boolean): void; setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx index 61caa4f9c3..d2a4f9a749 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor.tsx @@ -11,7 +11,7 @@ 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 { ConnectionExecutionContextResource, ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections'; +import { ConnectionExecutionContextResource, ConnectionInfoResource, createConnectionParam, DBDriverResource, getRealExecutionContextId } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NodeManagerUtils } from '@cloudbeaver/core-navigation-tree'; import { CaptureView } from '@cloudbeaver/core-view'; @@ -74,10 +74,7 @@ export const SqlEditor = observer(function SqlEditor({ state }) { SqlEditor, ConnectionInfoResource, dataSource?.executionContext - ? ({ - projectId: dataSource.executionContext.projectId, - connectionId: dataSource.executionContext.connectionId, - }) + ? createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId) : null ); const driver = useMapResource(SqlEditor, DBDriverResource, connection.data?.driverId ?? null); @@ -88,13 +85,13 @@ export const SqlEditor = observer(function SqlEditor({ state }) { const context = useMapResource( SqlEditor, ConnectionExecutionContextResource, - connected ? (dataSource?.executionContext?.id ?? null) : null + connected ? getRealExecutionContextId(dataSource?.executionContext?.id) : null ); const initializingContext = getComputed(() => connection.isLoading() || context.isLoading()); const initExecutionContext = getComputed(() => ( context.data === undefined - && dataSource?.executionContext !== undefined + && connection.data !== undefined )); async function cancelConnection() {