From 6d516a4f7ec5dba56683c8c4522ee23815ff6dc6 Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Thu, 8 Sep 2022 13:15:33 +0300 Subject: [PATCH 01/22] CB-2567 shared projects api --- .../rm/local/LocalResourceController.java | 35 +++++++++++-------- .../schema/service.rm.graphqls | 6 +++- .../cloudbeaver/service/rm/DBWServiceRM.java | 8 ++++- .../service/rm/WebServiceBindingRM.java | 4 +++ .../service/rm/impl/WebServiceRM.java | 24 ++++++++++--- 5 files changed, 57 insertions(+), 20 deletions(-) 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 c11891bbf7..f4366af8a7 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 @@ -123,20 +123,7 @@ public class LocalResourceController implements RMController { //TODO refactor after implement current user api in sm var activeUserCreds = credentialsProvider.getActiveUserCredentials(); if (Files.exists(sharedProjectsPath) && activeUserCreds != null && activeUserCreds.getUserId() != null) { - var accessibleSharedProjects = smController.getAllAvailableObjectsPermissions( - activeUserCreds.getUserId(), - SMObjects.PROJECT - ); - - projects = accessibleSharedProjects - .stream() - .map(projectPermission -> makeProjectFromPath( - sharedProjectsPath.resolve(projectPermission.getObjectId()), - projectPermission.getPermissions().stream().map(RMProjectPermission::fromPermission).collect(Collectors.toSet()), - RMProject.Type.SHARED, true) - ) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + projects = readAccessibleSharedProjects(activeUserCreds.getUserId()); } else { projects = new ArrayList<>(); } @@ -160,6 +147,26 @@ public class LocalResourceController implements RMController { return projects.toArray(new RMProject[0]); } + private List readAccessibleSharedProjects(@NotNull String userId) throws DBException { + if (credentialsProvider.hasPermission(DBWConstants.PERMISSION_ADMIN) || credentialsProvider.hasPermission(RMConstants.PERMISSION_RM_ADMIN)) { + return Arrays.asList(listAllSharedProjects()); + } + var accessibleSharedProjects = smController.getAllAvailableObjectsPermissions( + userId, + SMObjects.PROJECT + ); + + return accessibleSharedProjects + .stream() + .map(projectPermission -> makeProjectFromPath( + sharedProjectsPath.resolve(projectPermission.getObjectId()), + projectPermission.getPermissions().stream().map(RMProjectPermission::fromPermission).collect(Collectors.toSet()), + RMProject.Type.SHARED, true) + ) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + private Set getProjectPermissions(@Nullable String projectId, RMProject.Type projectType) throws DBException { var activeUserCreds = credentialsProvider.getActiveUserCredentials(); 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 1746a11e6e..53e824ac57 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -22,6 +22,10 @@ extend type Query { # List accessible projects rmListProjects: [RMProject!]! + rmListSharedProjects: [RMProject!]! + + rmProject(projectId: String!): RMProject! + # List accessible projects rmListResources( projectId: String!, @@ -62,7 +66,7 @@ extend type Mutation { rmCreateProject( projectName: String!, - description: String): ProjectInfo + description: String): RMProject rmDeleteProject(projectId: ID!): 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 9c0f7805c5..fbf53b2641 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 @@ -35,6 +35,12 @@ public interface DBWServiceRM extends DBWService { @WebAction RMProject[] listProjects(@NotNull WebSession webSession) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + RMProject[] listSharedProjects(@NotNull WebSession webSession) throws DBWebException; + + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + RMProject getProject(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException; + @NotNull @WebProjectAction( requireProjectPermissions = RMConstants.PERMISSION_PROJECT_RESOURCE_VIEW @@ -84,7 +90,7 @@ public interface DBWServiceRM extends DBWService { boolean forceOverwrite) throws DBException; @WebAction(requirePermissions = {DBWConstants.PERMISSION_ADMIN, RMConstants.PERMISSION_RM_ADMIN}) - WebProjectInfo createProject( + RMProject createProject( @NotNull WebSession session, @NotNull String name, @Nullable String description) throws DBWebException; 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 f1175d08ad..687a1a8e4a 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 @@ -38,6 +38,10 @@ public class WebServiceBindingRM extends WebServiceBindingBase { model.getQueryType() .dataFetcher("rmListProjects", env -> getService(env).listProjects(getWebSession(env))) + .dataFetcher("rmListSharedProjects", + env -> getService(env).listSharedProjects(getWebSession(env))) + .dataFetcher("rmProject", + env -> getService(env).getProject(getWebSession(env), env.getArgument("projectId"))) .dataFetcher("rmListResources", env -> getService(env).listResources(getWebSession(env), env.getArgument("projectId"), 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 92ef72c058..5fad3e0820 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 @@ -44,6 +44,24 @@ public class WebServiceRM implements DBWServiceRM { } } + @Override + public RMProject[] listSharedProjects(@NotNull WebSession webSession) throws DBWebException { + try { + return getResourceController(webSession).listAllSharedProjects(); + } catch (DBException e) { + throw new DBWebException("Error reading list of accessible projects", e); + } + } + + @Override + public RMProject getProject(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException { + try { + return getResourceController(webSession).getProject(projectId, false, false); + } catch (DBException e) { + throw new DBWebException("Error reading list of accessible projects", e); + } + } + @NotNull @Override public RMResource[] listResources(WebSession webSession, @NotNull String projectId, @Nullable String folder, @Nullable String nameMask, boolean readProperties, boolean readHistory) throws DBException { @@ -106,13 +124,11 @@ public class WebServiceRM implements DBWServiceRM { } @Override - public WebProjectInfo createProject( + public RMProject createProject( @NotNull WebSession session, @NotNull String name, @Nullable String description ) throws DBWebException { try { - RMProject rmProject = getResourceController(session).createProject(name, description); - var project = session.createVirtualProject(rmProject); - return new WebProjectInfo(session, project); + return getResourceController(session).createProject(name, description); } catch (DBException e) { throw new DBWebException("Error creating project", e); } From 3b23ecbf918f62de307f04af7ad04d7fd1fe86ed Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Thu, 8 Sep 2022 19:15:38 +0300 Subject: [PATCH 02/22] CB-2344 use mutable list --- .../io/cloudbeaver/model/rm/local/LocalResourceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f4366af8a7..5dc79c8373 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 @@ -149,7 +149,7 @@ public class LocalResourceController implements RMController { private List readAccessibleSharedProjects(@NotNull String userId) throws DBException { if (credentialsProvider.hasPermission(DBWConstants.PERMISSION_ADMIN) || credentialsProvider.hasPermission(RMConstants.PERMISSION_RM_ADMIN)) { - return Arrays.asList(listAllSharedProjects()); + return new ArrayList<>(Arrays.asList(listAllSharedProjects())); } var accessibleSharedProjects = smController.getAllAvailableObjectsPermissions( userId, From db564ec98142723328ae265494cc874943260825 Mon Sep 17 00:00:00 2001 From: Konstantin Vasilev Date: Fri, 9 Sep 2022 11:30:40 +0300 Subject: [PATCH 03/22] CB-2535 feat(plugin-connections): Show only private project for users in connection form --- .../src/ConnectionForm/Options/Options.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index ba10f04288..1e152733e9 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -117,7 +117,13 @@ export const Options: TabContainerPanelComponent = observe }, []); const projectsLoader = useMapResource(Options, ProjectsResource, CachedMapAllKey); - const projects = projectsLoader.data as Project[]; + const projects = (projectsLoader.data as Project[]).filter(p => { + if (!adminPermission && p.id === PROJECT_GLOBAL_ID) { + return false; + } + + return true; + }); function handleProjectSelect(projectId: string) { const project = projectsLoader.resource.get(projectId); From 591e3f28acf3e25d049b282c959e4ee25b369d5f Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Fri, 9 Sep 2022 12:21:23 +0300 Subject: [PATCH 04/22] CB-2344 grant all permissions for admin --- .../src/io/cloudbeaver/model/session/WebSession.java | 3 ++- .../src/io/cloudbeaver/service/WebServiceBindingBase.java | 2 +- .../src/io/cloudbeaver/service/rm/DBWServiceRM.java | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) 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 be9d1f1715..f1239c2260 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 @@ -242,7 +242,8 @@ public class WebSession extends AbstractSessionPersistent implements SMSession, } public synchronized boolean hasPermission(String perm) { - return getSessionPermissions().contains(perm); + return getSessionPermissions().contains(DBWConstants.PERMISSION_ADMIN) || + getSessionPermissions().contains(perm); } public synchronized boolean isAuthorizedInSecurityManager() { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 095ad4b3aa..99988a3d5c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -254,7 +254,7 @@ public abstract class WebServiceBindingBase impleme } // Check permissions for (String rp : reqPermissions) { - if (!sessionPermissions.contains(rp)) { + if (!session.hasPermission(rp)) { log.debug("Access to " + method.getName() + " denied for " + session.getUser()); throw new DBWebExceptionAccessDenied("Access denied"); } 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 fbf53b2641..15471ca396 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 @@ -89,13 +89,13 @@ public interface DBWServiceRM extends DBWService { @NotNull String data, boolean forceOverwrite) throws DBException; - @WebAction(requirePermissions = {DBWConstants.PERMISSION_ADMIN, RMConstants.PERMISSION_RM_ADMIN}) + @WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN}) RMProject createProject( @NotNull WebSession session, @NotNull String name, @Nullable String description) throws DBWebException; - @WebAction(requirePermissions = {DBWConstants.PERMISSION_ADMIN, RMConstants.PERMISSION_RM_ADMIN}) + @WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN}) boolean deleteProject( @NotNull WebSession session, @NotNull @WebObjectId String projectId) throws DBWebException; From baefb2f06fe3dcc9344d93e3ec2f55f46de7262b Mon Sep 17 00:00:00 2001 From: Konstantin Vasilev Date: Fri, 9 Sep 2022 13:13:54 +0300 Subject: [PATCH 05/22] CB-2535 feat(plugin-connections): Add project selector to connection form --- .../src/ConnectionForm/Options/Options.tsx | 41 +++---------- .../ConnectionForm/Options/ProjectSelect.tsx | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index 1e152733e9..9bbfe58150 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -34,9 +34,9 @@ import { import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { Project, ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; +import { PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { usePermission } from '@cloudbeaver/core-root'; -import { CachedMapAllKey, CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk'; +import { CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui'; import { useAuthenticationAction } from '@cloudbeaver/core-ui'; @@ -44,6 +44,7 @@ import { useAuthenticationAction } from '@cloudbeaver/core-ui'; import { ConnectionFormService } from '../ConnectionFormService'; import type { IConnectionFormProps } from '../IConnectionFormProps'; import { ParametersForm } from './ParametersForm'; +import { ProjectSelect } from './ProjectSelect'; import { useOptions } from './useOptions'; const styles = css` @@ -116,22 +117,6 @@ export const Options: TabContainerPanelComponent = observe optionsHook.setAuthModel(model); }, []); - const projectsLoader = useMapResource(Options, ProjectsResource, CachedMapAllKey); - const projects = (projectsLoader.data as Project[]).filter(p => { - if (!adminPermission && p.id === PROJECT_GLOBAL_ID) { - return false; - } - - return true; - }); - - function handleProjectSelect(projectId: string) { - const project = projectsLoader.resource.get(projectId); - - if (project?.canCreateConnections) { - state.setProject(projectId); - } - } const driverMap = useMapResource( Options, @@ -310,24 +295,12 @@ export const Options: TabContainerPanelComponent = observe > {translate('connections_connection_name')} - project.id} - valueSelector={project => project.name} - titleSelector={project => project.description} - isDisabled={project => !project.canCreateConnections} - searchable={projects.length > 10} - readOnly={readonly || edit || projects.length < 2} + readOnly={readonly || edit} disabled={disabled} - loading={projectsLoader.isLoading()} - tiny - fill - onSelect={handleProjectSelect} - > - {translate('connections_connection_project')} - + onChange={projectId => state.setProject(projectId)} + /> {!config.template && ( void; + readOnly?: boolean; + disabled?: boolean; +} + +export function ProjectSelect(props: Props) { + const translate = useTranslate(); + + const projectsLoader = useMapResource(ProjectSelect, ProjectsResource, CachedMapAllKey); + const projects = projectsLoader.data as Project[]; + + function handleProjectSelect(projectId: string) { + const project = projectsLoader.resource.get(projectId); + + if (project?.canCreateConnections) { + props.onChange(projectId); + } + } + + const possibleOptions = projects.filter(project => project.canCreateConnections); + + return ( + project.id} + valueSelector={project => project.name} + titleSelector={project => project.description} + isDisabled={project => !project.canCreateConnections} + readOnly={props.readOnly || possibleOptions.length <= 1} + searchable={projects.length > 10} + disabled={props.disabled} + loading={projectsLoader.isLoading()} + tiny + fill + onSelect={handleProjectSelect} + > + {translate('connections_connection_project')} + + ); +} \ No newline at end of file From 1e03c8c5420a1e6f142701372079f6e7ceb27954 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 9 Sep 2022 13:40:41 +0300 Subject: [PATCH 06/22] CB-2344 refactor: rename ProjectsResource --- webapp/packages/core-blocks/src/useMapResource.ts | 1 + .../{ProjectsResource.ts => ProjectInfoResource.ts} | 10 +++++----- .../core-projects/src/ProjectsNavNodeService.ts | 8 ++++---- webapp/packages/core-projects/src/ProjectsService.ts | 10 +++++----- webapp/packages/core-projects/src/index.ts | 2 +- webapp/packages/core-projects/src/manifest.ts | 4 ++-- .../CreateConnection/Manual/CustomConnection.tsx | 4 ++-- .../CreateConnection/Search/SearchDatabase.tsx | 4 ++-- .../Options/ConnectionOptionsTabService.ts | 4 ++-- .../src/ConnectionForm/Options/ProjectSelect.tsx | 6 +++--- .../src/NavNodes/ConnectionFoldersBootstrap.ts | 4 ++-- 11 files changed, 29 insertions(+), 28 deletions(-) rename webapp/packages/core-projects/src/{ProjectsResource.ts => ProjectInfoResource.ts} (70%) diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index 2a3c55b40d..2b51949149 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/useMapResource.ts @@ -55,6 +55,7 @@ interface IMapResourceState> loaded: boolean; resource: TResource; isOutdated: () => boolean; + reload: () => void; } interface IMapResourceListResult< diff --git a/webapp/packages/core-projects/src/ProjectsResource.ts b/webapp/packages/core-projects/src/ProjectInfoResource.ts similarity index 70% rename from webapp/packages/core-projects/src/ProjectsResource.ts rename to webapp/packages/core-projects/src/ProjectInfoResource.ts index 767ca16554..3918b39dca 100644 --- a/webapp/packages/core-projects/src/ProjectsResource.ts +++ b/webapp/packages/core-projects/src/ProjectInfoResource.ts @@ -10,12 +10,12 @@ import { runInAction } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; -import { GraphQLService, ProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk'; +import { GraphQLService, ProjectInfo as SchemaProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk'; -export type Project = ProjectInfo; +export type ProjectInfo = SchemaProjectInfo; @injectable() -export class ProjectsResource extends CachedMapResource { +export class ProjectInfoResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, private readonly userInfoResource: UserInfoResource, @@ -28,14 +28,14 @@ export class ProjectsResource extends CachedMapResource { }); } - protected async loader(key: ResourceKey): Promise> { + protected async loader(key: ResourceKey): Promise> { const all = ResourceKeyUtils.includes(key, CachedMapAllKey); const { projects } = await this.graphQLService.sdk.getProjectList(); runInAction(() => { if (all) { - this.data.clear(); + this.delete(resourceKeyList(this.keys.filter(id => !projects.some(project => project.id === id)))); } this.set(resourceKeyList(projects.map(project => project.id)), projects); diff --git a/webapp/packages/core-projects/src/ProjectsNavNodeService.ts b/webapp/packages/core-projects/src/ProjectsNavNodeService.ts index 611adf2ac9..f32f32fafc 100644 --- a/webapp/packages/core-projects/src/ProjectsNavNodeService.ts +++ b/webapp/packages/core-projects/src/ProjectsNavNodeService.ts @@ -11,18 +11,18 @@ import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { resourceKeyList } from '@cloudbeaver/core-sdk'; import { NAV_NODE_TYPE_PROJECT } from './NAV_NODE_TYPE_PROJECT'; -import { Project, ProjectsResource } from './ProjectsResource'; +import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource'; @injectable() export class ProjectsNavNodeService { constructor( private readonly navNodeInfoResource: NavNodeInfoResource, - private readonly projectsResource: ProjectsResource + private readonly projectsResource: ProjectInfoResource ) { } - getProject(nodeId: string): Project | undefined { + getProject(nodeId: string): ProjectInfo | undefined { const parentIds = [...this.navNodeInfoResource.getParents(nodeId), nodeId]; const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds)); @@ -35,7 +35,7 @@ export class ProjectsNavNodeService { return this.getByNodeId(projectNode.id); } - getByNodeId(nodeId: string): Project | undefined { + getByNodeId(nodeId: string): ProjectInfo | undefined { return this.projectsResource.get(nodeId.replace('resource://', '')); } diff --git a/webapp/packages/core-projects/src/ProjectsService.ts b/webapp/packages/core-projects/src/ProjectsService.ts index 1f2baf2d44..3416330894 100644 --- a/webapp/packages/core-projects/src/ProjectsService.ts +++ b/webapp/packages/core-projects/src/ProjectsService.ts @@ -12,12 +12,12 @@ import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; -import { Project, ProjectsResource } from './ProjectsResource'; +import { ProjectInfo, ProjectInfoResource } from './ProjectInfoResource'; @injectable() export class ProjectsService { - get activeProject(): Project | undefined { - let project: Project | undefined; + get activeProject(): ProjectInfo | undefined { + let project: ProjectInfo | undefined; if (this.activeProjectId) { project = this.projectsResource.get(this.activeProjectId); @@ -41,7 +41,7 @@ export class ProjectsService { private activeProjectId: string | null; constructor( - private readonly projectsResource: ProjectsResource, + private readonly projectsResource: ProjectInfoResource, private readonly userInfoResource: UserInfoResource ) { this.activeProjectId = null; @@ -51,7 +51,7 @@ export class ProjectsService { }); } - setActiveProject(project: Project): void { + setActiveProject(project: ProjectInfo): void { this.activeProjectId = project.id; } diff --git a/webapp/packages/core-projects/src/index.ts b/webapp/packages/core-projects/src/index.ts index bc23ee59bf..e64f70432d 100644 --- a/webapp/packages/core-projects/src/index.ts +++ b/webapp/packages/core-projects/src/index.ts @@ -1,4 +1,4 @@ -export * from './ProjectsResource'; +export * from './ProjectInfoResource'; export * from './ProjectsService'; export * from './NAV_NODE_TYPE_PROJECT'; export * from './PROJECT_GLOBAL_ID'; diff --git a/webapp/packages/core-projects/src/manifest.ts b/webapp/packages/core-projects/src/manifest.ts index aba58ae383..990808529f 100644 --- a/webapp/packages/core-projects/src/manifest.ts +++ b/webapp/packages/core-projects/src/manifest.ts @@ -9,8 +9,8 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; +import { ProjectInfoResource } from './ProjectInfoResource'; import { ProjectsNavNodeService } from './ProjectsNavNodeService'; -import { ProjectsResource } from './ProjectsResource'; import { ProjectsService } from './ProjectsService'; export const manifest: PluginManifest = { @@ -19,7 +19,7 @@ export const manifest: PluginManifest = { }, providers: [ - ProjectsResource, + ProjectInfoResource, ProjectsService, LocaleService, ProjectsNavNodeService, 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 f42d3a6b95..f09fe57db1 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 @@ -13,7 +13,7 @@ import { useMemo } from 'react'; import { Loader, useMapResource } from '@cloudbeaver/core-blocks'; import { DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; -import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; +import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { ConnectionManualService } from './ConnectionManualService'; @@ -27,7 +27,7 @@ export const CustomConnection = observer(function CustomConnection() { dbDriverResource.resource.enabledDrivers.slice().sort(dbDriverResource.resource.compare) )), [dbDriverResource]); - useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey); + useMapResource(CustomConnection, ProjectInfoResource, CachedMapAllKey); function select(driverId: string) { connectionManualService.select(PROJECT_GLOBAL_ID, driverId); diff --git a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx index f0ddab4d99..5b8e615607 100644 --- a/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx +++ b/webapp/packages/plugin-connections-administration/src/Administration/Connections/CreateConnection/Search/SearchDatabase.tsx @@ -11,7 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useMapResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; +import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, type AdminConnectionSearchInfo } from '@cloudbeaver/core-sdk'; import { CustomConnection } from '../Manual/CustomConnection'; @@ -20,7 +20,7 @@ import { DatabaseList } from './DatabaseList'; export const SearchDatabase: React.FC = observer(function SearchDatabase() { const connectionSearchService = useService(ConnectionSearchService); - useMapResource(CustomConnection, ProjectsResource, CachedMapAllKey); + useMapResource(CustomConnection, ProjectInfoResource, CachedMapAllKey); function select(database: AdminConnectionSearchInfo) { connectionSearchService.select(PROJECT_GLOBAL_ID, database); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index acc6f743e9..b0712e1f95 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -11,7 +11,7 @@ import { action, makeObservable, runInAction, toJS } from 'mobx'; import { createConnectionParam, DatabaseAuthModelsResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; +import { ProjectInfoResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { getUniqueName, isValuesEqual } from '@cloudbeaver/core-utils'; @@ -26,7 +26,7 @@ import { Options } from './Options'; @injectable() export class ConnectionOptionsTabService extends Bootstrap { constructor( - private readonly projectsResource: ProjectsResource, + private readonly projectsResource: ProjectInfoResource, private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource, private readonly databaseAuthModelsResource: DatabaseAuthModelsResource, diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx index d57ab1917e..0eb22f1e31 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx @@ -10,7 +10,7 @@ import type React from 'react'; import { Combobox, useMapResource } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { Project, ProjectsResource } from '@cloudbeaver/core-projects'; +import { ProjectInfo, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; @@ -24,8 +24,8 @@ interface Props { export function ProjectSelect(props: Props) { const translate = useTranslate(); - const projectsLoader = useMapResource(ProjectSelect, ProjectsResource, CachedMapAllKey); - const projects = projectsLoader.data as Project[]; + const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey); + const projects = projectsLoader.data as ProjectInfo[]; function handleProjectSelect(projectId: string) { const project = projectsLoader.resource.get(projectId); diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index 5f1b4513b3..b47d89ae75 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -17,7 +17,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { ENodeMoveType, getNodesFromContext, INodeMoveData, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, nodeDeleteContext } from '@cloudbeaver/core-navigation-tree'; -import { NAV_NODE_TYPE_PROJECT, ProjectsNavNodeService, ProjectsResource } from '@cloudbeaver/core-projects'; +import { NAV_NODE_TYPE_PROJECT, ProjectsNavNodeService, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { createPath } from '@cloudbeaver/core-utils'; import { ActionService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, MenuService } from '@cloudbeaver/core-view'; @@ -47,7 +47,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { private readonly commonDialogService: CommonDialogService, private readonly notificationService: NotificationService, private readonly navNodeInfoResource: NavNodeInfoResource, - private readonly projectsResource: ProjectsResource, + private readonly projectsResource: ProjectInfoResource, private readonly projectsNavNodeService: ProjectsNavNodeService ) { super(); From 865d6452a03e08f51ceec913221f9350ec4ffd77 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 9 Sep 2022 13:43:01 +0300 Subject: [PATCH 07/22] CB-2344 feat: core-resource-manager --- .../schema/service.rm.graphqls | 5 +- .../packages/core-bootstrap/src/manifest.ts | 2 + .../packages/core-resource-manager/.gitignore | 21 ++++ .../core-resource-manager/package.json | 26 ++++ .../src/SharedProjectsResource.ts | 111 ++++++++++++++++++ .../core-resource-manager/src/index.ts | 2 + .../core-resource-manager/src/manifest.ts | 22 ++++ .../core-resource-manager/tsconfig.json | 11 ++ .../src/queries/fragments/SharedProject.gql | 7 ++ .../resource-manager/createProject.gql | 13 ++ .../resource-manager/deleteProject.gql | 3 + .../queries/resource-manager/getProject.gql | 7 ++ .../resource-manager/getSharedProjects.gql | 5 + webapp/packages/core-sdk/src/sdk.ts | 94 ++++++++++++++- 14 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 webapp/packages/core-resource-manager/.gitignore create mode 100644 webapp/packages/core-resource-manager/package.json create mode 100644 webapp/packages/core-resource-manager/src/SharedProjectsResource.ts create mode 100644 webapp/packages/core-resource-manager/src/index.ts create mode 100644 webapp/packages/core-resource-manager/src/manifest.ts create mode 100644 webapp/packages/core-resource-manager/tsconfig.json create mode 100644 webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql create mode 100644 webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql create mode 100644 webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql create mode 100644 webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql create mode 100644 webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql 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 53e824ac57..ad8f6bf6f5 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -1,7 +1,7 @@ # Metadata queries type RMProject { - id: String! + id: ID! name: String! description: String shared: Boolean! @@ -65,8 +65,9 @@ extend type Mutation { forceOverwrite: Boolean!): String! rmCreateProject( + projectId: ID, projectName: String!, - description: String): RMProject + description: String): ProjectInfo! rmDeleteProject(projectId: ID!): Boolean! } \ No newline at end of file diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 394e2fafaa..9f930b14f5 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -20,6 +20,7 @@ import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; import { corePluginManifest } from '@cloudbeaver/core-plugin'; import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; +import { resourceManagerManifest } from '@cloudbeaver/core-resource-manager'; import { coreRootManifest } from '@cloudbeaver/core-root'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; @@ -61,6 +62,7 @@ export const coreManifests: PluginManifest[] = [ coreConnectionsManifest, coreAdministrationManifest, codeDialogsManifest, + resourceManagerManifest, coreAppManifest, coreNavigationTree, ]; diff --git a/webapp/packages/core-resource-manager/.gitignore b/webapp/packages/core-resource-manager/.gitignore new file mode 100644 index 0000000000..99902db863 --- /dev/null +++ b/webapp/packages/core-resource-manager/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/core-resource-manager/package.json b/webapp/packages/core-resource-manager/package.json new file mode 100644 index 0000000000..638e6fc3ef --- /dev/null +++ b/webapp/packages/core-resource-manager/package.json @@ -0,0 +1,26 @@ +{ + "name": "@cloudbeaver/core-resource-manager", + "sideEffects": false, + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "lib/index.js", + "module": "lib/index.js", + "types": "src/index.ts", + "scripts": { + "prebuild": "rimraf lib/*", + "build": "webpack --config ../../configs/webpack.plugin.config.js", + "postbuild": "tsc", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" + }, + "peerDependencies": { + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-authentication": "~0.1.0", + "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-navigation-tree": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0" + } +} diff --git a/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts new file mode 100644 index 0000000000..2af647bd86 --- /dev/null +++ b/webapp/packages/core-resource-manager/src/SharedProjectsResource.ts @@ -0,0 +1,111 @@ +/* + * 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 { runInAction } from 'mobx'; + +import { EAdminPermission } from '@cloudbeaver/core-administration'; +import { injectable } from '@cloudbeaver/core-di'; +import { SessionPermissionsResource } from '@cloudbeaver/core-root'; +import { GraphQLService, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList, RmProject, ResourceKeyList } from '@cloudbeaver/core-sdk'; + +const newSymbol = Symbol('new-project'); + +export type SharedProject = RmProject; +type SharedProjectNew = SharedProject & { [newSymbol]: boolean }; + +interface IProjectConfig { + id: string; + name: string; + description?: string; +} + +@injectable() +export class SharedProjectsResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + sessionPermissionsResource: SessionPermissionsResource, + ) { + super([]); + + sessionPermissionsResource + .require(this, EAdminPermission.admin); + this.sync(sessionPermissionsResource, () => {}); + } + + isNew(id: string): boolean { + if (!this.has(id)) { + return true; + } + return newSymbol in this.get(id)!; + } + + async create(config: IProjectConfig): Promise { + const { project } = await this.graphQLService.sdk.createProject({ + projectId: config.id, + projectName: config.name, + description: config.description, + }); + + (project as SharedProjectNew)[newSymbol] = true; + + this.set(project.id, project as SharedProject); + + return this.get(project.id)!; + } + + async deleteProject(id: string): Promise; + async deleteProject(id: ResourceKeyList): Promise; + async deleteProject(key: ResourceKey): Promise { + const deleted: string[] = []; + + try { + await this.performUpdate(key, undefined, async key => { + await ResourceKeyUtils.forEachAsync(this.transformParam(key), async projectId => { + await this.graphQLService.sdk.deleteProject({ + projectId, + }); + + deleted.push(projectId); + }); + }); + } finally { + this.delete(resourceKeyList(deleted)); + } + } + + protected async loader(key: ResourceKey): Promise> { + const all = ResourceKeyUtils.includes(key, CachedMapAllKey); + + if (all) { + const { projects } = await this.graphQLService.sdk.getSharedProjects(); + + runInAction(() => { + this.delete(resourceKeyList(this.keys.filter(id => !projects.some(project => project.id === id)))); + + this.set(resourceKeyList(projects.map(project => project.id)), projects as SharedProject[]); + }); + } else { + await ResourceKeyUtils.forEachAsync(this.transformParam(key), async projectId => { + const { project } = await this.graphQLService.sdk.getProject({ + projectId, + }); + + this.set(projectId, project as SharedProject); + }); + } + + return this.data; + } + + protected dataSet(key: string, value: SharedProject): void { + key = this.getKeyRef(key); + + const data = this.data.get(key); + this.data.set(key, Object.assign(data ?? {}, value)); + } +} \ No newline at end of file diff --git a/webapp/packages/core-resource-manager/src/index.ts b/webapp/packages/core-resource-manager/src/index.ts new file mode 100644 index 0000000000..1a1224277b --- /dev/null +++ b/webapp/packages/core-resource-manager/src/index.ts @@ -0,0 +1,2 @@ +export * from './SharedProjectsResource'; +export { resourceManagerManifest } from './manifest'; diff --git a/webapp/packages/core-resource-manager/src/manifest.ts b/webapp/packages/core-resource-manager/src/manifest.ts new file mode 100644 index 0000000000..beeba9dc78 --- /dev/null +++ b/webapp/packages/core-resource-manager/src/manifest.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 type { PluginManifest } from '@cloudbeaver/core-di'; + +import { SharedProjectsResource } from './SharedProjectsResource'; + + +export const resourceManagerManifest: PluginManifest = { + info: { + name: 'Resource Manager Core', + }, + + providers: [ + SharedProjectsResource, + ], +}; diff --git a/webapp/packages/core-resource-manager/tsconfig.json b/webapp/packages/core-resource-manager/tsconfig.json new file mode 100644 index 0000000000..2a4cc370f7 --- /dev/null +++ b/webapp/packages/core-resource-manager/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "composite": true + }, + "include": ["src"], + "exclude": ["**/node_modules", "**/.*/"], + "references": [{ "path": "../*" }] +} diff --git a/webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql b/webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql new file mode 100644 index 0000000000..8cc0ddb4bd --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/fragments/SharedProject.gql @@ -0,0 +1,7 @@ +fragment SharedProject on RMProject { + id + name + shared + description + projectPermissions +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql b/webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql new file mode 100644 index 0000000000..61a4ee7018 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/createProject.gql @@ -0,0 +1,13 @@ +mutation createProject( + $projectId: ID + $projectName: String! + $description: String +) { + project: rmCreateProject( + projectId: $projectId + projectName: $projectName + description: $description + ) { + ...SharedProject + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql b/webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql new file mode 100644 index 0000000000..609c981045 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/deleteProject.gql @@ -0,0 +1,3 @@ +mutation deleteProject($projectId: ID!) { + rmDeleteProject(projectId: $projectId) +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql b/webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql new file mode 100644 index 0000000000..22e99e24c2 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/getProject.gql @@ -0,0 +1,7 @@ +query getProject($projectId: String!) { + project: rmProject( + projectId: $projectId + ) { + ...SharedProject + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql b/webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql new file mode 100644 index 0000000000..c685b0e137 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/resource-manager/getSharedProjects.gql @@ -0,0 +1,5 @@ +query getSharedProjects { + projects: rmListSharedProjects { + ...SharedProject + } +} \ 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 5ae56d4f47..af8f1e5f66 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -330,6 +330,7 @@ export interface DriverInfo { promotedScore?: Maybe; providerId?: Maybe; providerProperties: Array; + requiresDatabaseName?: Maybe; requiresServerName?: Maybe; sampleURL?: Maybe; } @@ -365,7 +366,7 @@ export interface Mutation { openSession: SessionInfo; readLobValue: Scalars['String']; refreshSessionConnections?: Maybe; - rmCreateProject?: Maybe; + rmCreateProject: RmProject; rmCreateResource: Scalars['String']; rmDeleteProject: Scalars['Boolean']; rmDeleteResource?: Maybe; @@ -531,6 +532,7 @@ export interface MutationReadLobValueArgs { export interface MutationRmCreateProjectArgs { description?: InputMaybe; + projectId?: InputMaybe; projectName: Scalars['String']; } @@ -861,6 +863,8 @@ export interface Query { revokeUserRole?: Maybe; rmListProjects: Array; rmListResources: Array; + rmListSharedProjects: Array; + rmProject: RmProject; rmReadResourceAsString: Scalars['String']; saveAuthProviderConfiguration: AdminAuthProviderConfiguration; saveUserMetaParameter: ObjectPropertyInfo; @@ -1114,6 +1118,11 @@ export interface QueryRmListResourcesArgs { } +export interface QueryRmProjectArgs { + projectId: Scalars['String']; +} + + export interface QueryRmReadResourceAsStringArgs { projectId: Scalars['String']; resourcePath: Scalars['String']; @@ -1276,7 +1285,7 @@ export interface RmProject { createTime: Scalars['DateTime']; creator: Scalars['String']; description?: Maybe; - id: Scalars['String']; + id: Scalars['ID']; name: Scalars['String']; projectPermissions: Array; shared: Scalars['Boolean']; @@ -2188,6 +2197,8 @@ export type SqlScriptInfoFragment = { queries: Array<{ start: number, end: numbe export type SessionStateFragment = { createTime: string, lastAccessTime: string, cacheExpired: boolean, locale: string, actionParameters?: any, valid: boolean, remainingTime: number }; +export type SharedProjectFragment = { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array }; + export type UserConnectionAuthPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array, defaultValue?: any, length: ObjectPropertyLength, features: Array, order: number }; export type UserConnectionNetworkHandlerPropertiesFragment = { id?: string, displayName?: string, description?: string, category?: string, dataType?: string, value?: any, validValues?: Array, defaultValue?: any, order: number, length: ObjectPropertyLength, features: Array }; @@ -2392,6 +2403,15 @@ export type GetProjectListQueryVariables = Exact<{ [key: string]: never; }>; export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canCreateConnections: boolean, canViewConnections: boolean }> }; +export type CreateProjectMutationVariables = Exact<{ + projectId?: InputMaybe; + projectName: Scalars['String']; + description?: InputMaybe; +}>; + + +export type CreateProjectMutation = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array } }; + export type CreateResourceMutationVariables = Exact<{ projectId: Scalars['String']; resourcePath: Scalars['String']; @@ -2401,6 +2421,13 @@ export type CreateResourceMutationVariables = Exact<{ export type CreateResourceMutation = { rmCreateResource: string }; +export type DeleteProjectMutationVariables = Exact<{ + projectId: Scalars['ID']; +}>; + + +export type DeleteProjectMutation = { rmDeleteProject: boolean }; + export type DeleteResourceMutationVariables = Exact<{ projectId: Scalars['String']; resourcePath: Scalars['String']; @@ -2410,6 +2437,13 @@ export type DeleteResourceMutationVariables = Exact<{ export type DeleteResourceMutation = { rmDeleteResource?: boolean }; +export type GetProjectQueryVariables = Exact<{ + projectId: Scalars['String']; +}>; + + +export type GetProjectQuery = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array } }; + export type GetResourceListQueryVariables = Exact<{ projectId: Scalars['String']; folder?: InputMaybe; @@ -2426,6 +2460,11 @@ export type GetResourceProjectListQueryVariables = Exact<{ [key: string]: never; export type GetResourceProjectListQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array }> }; +export type GetSharedProjectsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetSharedProjectsQuery = { projects: Array<{ id: string, name: string, shared: boolean, description?: string, projectPermissions: Array }> }; + export type MoveResourceMutationVariables = Exact<{ projectId: Scalars['String']; oldPath: Scalars['String']; @@ -2867,6 +2906,15 @@ export const SessionStateFragmentDoc = ` remainingTime } `; +export const SharedProjectFragmentDoc = ` + fragment SharedProject on RMProject { + id + name + shared + description + projectPermissions +} + `; export const UserConnectionNetworkHandlerPropertiesFragmentDoc = ` fragment UserConnectionNetworkHandlerProperties on ObjectPropertyInfo { id @@ -3847,6 +3895,17 @@ export const GetProjectListDocument = ` } } `; +export const CreateProjectDocument = ` + mutation createProject($projectId: ID, $projectName: String!, $description: String) { + project: rmCreateProject( + projectId: $projectId + projectName: $projectName + description: $description + ) { + ...SharedProject + } +} + ${SharedProjectFragmentDoc}`; export const CreateResourceDocument = ` mutation createResource($projectId: String!, $resourcePath: String!, $isFolder: Boolean!) { rmCreateResource( @@ -3856,6 +3915,11 @@ export const CreateResourceDocument = ` ) } `; +export const DeleteProjectDocument = ` + mutation deleteProject($projectId: ID!) { + rmDeleteProject(projectId: $projectId) +} + `; export const DeleteResourceDocument = ` mutation deleteResource($projectId: String!, $resourcePath: String!, $recursive: Boolean!) { rmDeleteResource( @@ -3865,6 +3929,13 @@ export const DeleteResourceDocument = ` ) } `; +export const GetProjectDocument = ` + query getProject($projectId: String!) { + project: rmProject(projectId: $projectId) { + ...SharedProject + } +} + ${SharedProjectFragmentDoc}`; export const GetResourceListDocument = ` query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) { resources: rmListResources( @@ -3891,6 +3962,13 @@ export const GetResourceProjectListDocument = ` } } `; +export const GetSharedProjectsDocument = ` + query getSharedProjects { + projects: rmListSharedProjects { + ...SharedProject + } +} + ${SharedProjectFragmentDoc}`; export const MoveResourceDocument = ` mutation moveResource($projectId: String!, $oldPath: String!, $newPath: String!) { rmMoveResource( @@ -4401,18 +4479,30 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = getProjectList(variables?: GetProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectList', 'query'); }, + createProject(variables: CreateProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(CreateProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createProject', 'mutation'); + }, createResource(variables: CreateResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(CreateResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'createResource', 'mutation'); }, + deleteProject(variables: DeleteProjectMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(DeleteProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteProject', 'mutation'); + }, deleteResource(variables: DeleteResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(DeleteResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteResource', 'mutation'); }, + getProject(variables: GetProjectQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProject', 'query'); + }, getResourceList(variables: GetResourceListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetResourceListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceList', 'query'); }, getResourceProjectList(variables?: GetResourceProjectListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(GetResourceProjectListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceProjectList', 'query'); }, + getSharedProjects(variables?: GetSharedProjectsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetSharedProjectsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getSharedProjects', 'query'); + }, moveResource(variables: MoveResourceMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(MoveResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'moveResource', 'mutation'); }, From e3a611a89cd21dd9635266fc804eee5ac7b8044a Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 9 Sep 2022 13:44:30 +0300 Subject: [PATCH 08/22] CB-2344 fix: gql schema --- .../io.cloudbeaver.service.rm/schema/service.rm.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ad8f6bf6f5..3d3e6f8050 100644 --- a/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls +++ b/server/bundles/io.cloudbeaver.service.rm/schema/service.rm.graphqls @@ -67,7 +67,7 @@ extend type Mutation { rmCreateProject( projectId: ID, projectName: String!, - description: String): ProjectInfo! + description: String): RMProject! rmDeleteProject(projectId: ID!): Boolean! } \ No newline at end of file From 21afbe21cdb8392df172fd24ef408642e4e40dfd Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 9 Sep 2022 14:23:04 +0300 Subject: [PATCH 09/22] CB-2344 refactor: dependency injection for execution context --- .../core-blocks/src/StatusMessage.tsx | 32 +++++++++++++------ .../packages/core-blocks/src/Table/Table.tsx | 19 ++++++++++- webapp/packages/core-di/src/DIService.ts | 11 +++++-- .../core-di/src/dependencyInjectorContext.ts | 15 +++++++++ webapp/packages/core-di/src/index.ts | 1 + .../core-executor/src/ExecutionContext.ts | 30 +++++++++++++---- webapp/packages/core-executor/src/Executor.ts | 17 ++++++++-- .../src/ExecutorHandlersCollection.ts | 19 +++++++++++ .../core-executor/src/IExecutionContext.ts | 6 ++++ .../src/IExecutorHandlersCollection.ts | 12 ++++++- .../core-executor/src/SyncExecutor.ts | 16 +++++++++- .../src/executionExceptionContext.ts | 16 ++++++++++ webapp/packages/core-executor/src/index.ts | 1 + .../core-projects/src/ProjectInfoResource.ts | 5 +++ 14 files changed, 176 insertions(+), 24 deletions(-) create mode 100644 webapp/packages/core-di/src/dependencyInjectorContext.ts create mode 100644 webapp/packages/core-executor/src/executionExceptionContext.ts diff --git a/webapp/packages/core-blocks/src/StatusMessage.tsx b/webapp/packages/core-blocks/src/StatusMessage.tsx index a2006d549b..1ef76d3178 100644 --- a/webapp/packages/core-blocks/src/StatusMessage.tsx +++ b/webapp/packages/core-blocks/src/StatusMessage.tsx @@ -13,12 +13,7 @@ import { IconOrImage, Link } from '@cloudbeaver/core-blocks'; import { ENotificationType } from '@cloudbeaver/core-events'; import { useTranslate } from '@cloudbeaver/core-localization'; -interface Props { - message: string | undefined | null; - status: ENotificationType | undefined; - onShowDetails?: () => void; - className?: string; -} +import { useErrorDetails } from './useErrorDetails'; const styles = css` status-message { @@ -43,17 +38,36 @@ const styles = css` } `; -export const StatusMessage = observer(function StatusMessage({ status, message, onShowDetails, className }) { +interface Props { + message?: string | null; + status?: ENotificationType; + exception?: Error | null; + onShowDetails?: () => void; + className?: string; +} + +export const StatusMessage = observer(function StatusMessage({ + status, + message, + exception = null, + onShowDetails, + className, +}) { const translate = useTranslate(); - message = message ? translate(message) : message; + const errorDetails = useErrorDetails(exception); + message = (message ? translate(message) : message) ?? errorDetails.details?.name; let icon = '/icons/info_icon.svg'; - if (status === ENotificationType.Error) { + if (status === ENotificationType.Error || exception !== null) { icon = '/icons/error_icon.svg'; } else if (status === ENotificationType.Success) { icon = '/icons/success_icon.svg'; } + if (errorDetails.details?.hasDetails && !onShowDetails) { + onShowDetails = errorDetails.open; + } + return styled(styles)( {message && ( diff --git a/webapp/packages/core-blocks/src/Table/Table.tsx b/webapp/packages/core-blocks/src/Table/Table.tsx index 5b3ae0bfa1..2e5b3a194e 100644 --- a/webapp/packages/core-blocks/src/Table/Table.tsx +++ b/webapp/packages/core-blocks/src/Table/Table.tsx @@ -8,7 +8,7 @@ import { observable, computed, action } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import styled, { use } from 'reshadow'; import { useObjectRef } from '../useObjectRef'; @@ -34,6 +34,23 @@ export const Table = observer>(function Table({ const [selected] = useState>(() => selectedItems || observable(new Map())); const [expanded] = useState>(() => expandedItems || observable(new Map())); + useEffect(action(() => { + if (!keys) { + return; + } + + const removeSelected = Array.from(selected.keys()).filter(key => !keys.includes(key)); + const removeExpanded = Array.from(expanded.keys()).filter(key => !keys.includes(key)); + + for (const id of removeSelected) { + selected.delete(id); + } + + for (const id of removeExpanded) { + expanded.delete(id); + } + }), [keys]); + const state: ITableState = useObservableRef(() => ({ get selectableItems() { if (!this.keys) { diff --git a/webapp/packages/core-di/src/DIService.ts b/webapp/packages/core-di/src/DIService.ts index d9e12f8672..1b185da8d8 100644 --- a/webapp/packages/core-di/src/DIService.ts +++ b/webapp/packages/core-di/src/DIService.ts @@ -6,9 +6,10 @@ * you may not use this file except in compliance with the License. */ -import type { ISyncContextLoader } from '@cloudbeaver/core-executor'; +import type { IExecutorHandlersCollection, ISyncContextLoader } from '@cloudbeaver/core-executor'; -import type { App } from './App'; +import { App } from './App'; +import { dependencyInjectorContext } from './dependencyInjectorContext'; import type { IServiceConstructor, IServiceInjector } from './IApp'; import { injectable } from './injectable'; @@ -22,7 +23,11 @@ export class DIService { private readonly app: App ) { } - dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = ( + addDIContext(context: IExecutorHandlersCollection): void { + context.addContextCreator(dependencyInjectorContext, this.dependencyInjectorContext); + } + + private readonly dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = ( () => this.serviceInjector.getServiceByClass.bind(this) ); } \ No newline at end of file diff --git a/webapp/packages/core-di/src/dependencyInjectorContext.ts b/webapp/packages/core-di/src/dependencyInjectorContext.ts new file mode 100644 index 0000000000..40bd182d1c --- /dev/null +++ b/webapp/packages/core-di/src/dependencyInjectorContext.ts @@ -0,0 +1,15 @@ +/* + * 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 { ISyncContextLoader } from '@cloudbeaver/core-executor'; + +import type { IServiceConstructor } from './IApp'; + +export const dependencyInjectorContext: ISyncContextLoader<(ctor: IServiceConstructor) => T> = function dependencyInjectorContext() { + throw new Error('Implementation not provided \n Use addContextCreator to add addDIContext implementation to context'); +}; \ No newline at end of file diff --git a/webapp/packages/core-di/src/index.ts b/webapp/packages/core-di/src/index.ts index 8096df9cf7..ef435878ce 100644 --- a/webapp/packages/core-di/src/index.ts +++ b/webapp/packages/core-di/src/index.ts @@ -3,6 +3,7 @@ export * from './App'; export * from './AppContext'; export * from './Bootstrap'; export * from './Dependency'; +export * from './dependencyInjectorContext'; export * from './DIService'; export * from './injectable'; export * from './PluginManifest'; diff --git a/webapp/packages/core-executor/src/ExecutionContext.ts b/webapp/packages/core-executor/src/ExecutionContext.ts index d27501ccb8..032981993d 100644 --- a/webapp/packages/core-executor/src/ExecutionContext.ts +++ b/webapp/packages/core-executor/src/ExecutionContext.ts @@ -10,23 +10,39 @@ import type { IAsyncContextLoader, IContextLoader, IExecutionContext, ISyncConte export class ExecutionContext implements IExecutionContext { readonly contexts: Map, any>; + readonly contextCreators: Map, IContextLoader>; - constructor(private data: TData, context?: IExecutionContext) { - this.contexts = context?.contexts || new Map, any>(); + constructor(private readonly data: TData, context?: IExecutionContext) { + this.contexts = context?.contexts || new Map(); + this.contextCreators = context?.contextCreators ?? new Map(); } - hasContext(loader: IContextLoader): boolean { - return this.contexts.has(loader); + addContextCreators( + creators: [IContextLoader, IContextLoader][] + ): void { + for (const [key, value] of creators) { + this.contextCreators.set(key, value); + } } - getContext(token: ISyncContextLoader): T - getContext(token: IAsyncContextLoader): Promise + hasContext(loader: IContextLoader): boolean { + return this.contextCreators.has(loader) || this.contexts.has(loader); + } + + getContext(token: ISyncContextLoader): T; + getContext(token: IAsyncContextLoader): Promise; getContext(token: IContextLoader): Promise | T { if (this.contexts.has(token)) { return this.contexts.get(token); } - const value = token(this, this.data); + let value: T | Promise; + + if (this.contextCreators.has(token)) { + value = this.contextCreators.get(token)!(this, this.data); + } else { + value = token(this, this.data); + } if (value instanceof Promise) { return this.getAsyncContext(token, value); diff --git a/webapp/packages/core-executor/src/Executor.ts b/webapp/packages/core-executor/src/Executor.ts index f4fb934f20..d5cf73bb5c 100644 --- a/webapp/packages/core-executor/src/Executor.ts +++ b/webapp/packages/core-executor/src/Executor.ts @@ -9,6 +9,7 @@ import { flat } from '@cloudbeaver/core-utils'; import { ExecutionContext } from './ExecutionContext'; +import { executionExceptionContext } from './executionExceptionContext'; import { ExecutorHandlersCollection } from './ExecutorHandlersCollection'; import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter'; import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext'; @@ -68,9 +69,16 @@ export class Executor extends ExecutorHandlersCollection implements context: IExecutionContext, scoped: Array> ): Promise> { - const interrupter = context.getContext(ExecutorInterrupter.interruptContext); scoped = [...collection.collections, ...scoped]; + context.addContextCreators(collection.contextCreators as any); + + for (const scope of scoped) { + context.addContextCreators(scope.contextCreators as any); + } + + const interrupter = context.getContext(ExecutorInterrupter.interruptContext); + await this.executeChain(collection, data, context, 'before'); for (const scope of scoped) { @@ -83,7 +91,11 @@ export class Executor extends ExecutorHandlersCollection implements for (const scope of scoped) { await this.executeHandlers(data, context, scope.handlers, interrupter); } - } finally { + } catch (exception: any) { + const exceptionContext = context.getContext(executionExceptionContext); + exceptionContext.setException(exception); + throw exception; + } finally { await this.executeHandlers(data, context, collection.postHandlers); for (const scope of scoped) { @@ -158,6 +170,7 @@ export class Executor extends ExecutorHandlersCollection implements this.scheduler.schedule(data, async () => { const context = new ExecutionContext(data); + context.addContextCreators(this.contextCreators as any); try { await handler(data, context); diff --git a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts index 4bc69c555a..26504a4812 100644 --- a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts @@ -8,6 +8,7 @@ import { makeObservable, observable } from 'mobx'; +import type { IContextLoader, ISyncContextLoader, IAsyncContextLoader } from './IExecutionContext'; import type { IExecutorHandler } from './IExecutorHandler'; import type { ExecutorDataFilter, ExecutorDataMap, IChainLink, IExecutorHandlersCollection } from './IExecutorHandlersCollection'; @@ -16,12 +17,14 @@ implements IExecutorHandlersCollection { handlers: Array> = []; postHandlers: Array> = []; chain: Array> = []; + readonly contextCreators: Map, IContextLoader>; readonly collections: Array>; protected initialDataGetter: (() => T) | null; private readonly links: Map, IExecutorHandlersCollection>; constructor() { this.links = new Map(); + this.contextCreators = new Map(); this.collections = []; this.initialDataGetter = null; @@ -34,6 +37,22 @@ implements IExecutorHandlersCollection { }); } + addContextCreator( + context: ISyncContextLoader, + creator: ISyncContextLoader + ): this; + addContextCreator( + context: IAsyncContextLoader, + creator: IAsyncContextLoader + ): this; + addContextCreator( + context: IContextLoader, + creator: IContextLoader + ): this { + this.contextCreators.set(context, creator); + return this; + } + setInitialDataGetter(getter: (() => T) | null): this { this.initialDataGetter = getter; return this; diff --git a/webapp/packages/core-executor/src/IExecutionContext.ts b/webapp/packages/core-executor/src/IExecutionContext.ts index 7f4f5d3cc8..2feade714e 100644 --- a/webapp/packages/core-executor/src/IExecutionContext.ts +++ b/webapp/packages/core-executor/src/IExecutionContext.ts @@ -17,7 +17,13 @@ export interface IExecutionContextProvider { } export interface IExecutionContext extends IExecutionContextProvider { + readonly parent?: IExecutionContext; readonly contexts: Map, any>; + readonly contextCreators: Map, IContextLoader>; + + addContextCreators( + creators: [IContextLoader, IContextLoader][] + ): void; } export type IAsyncContextLoader = ( diff --git a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts index 2cc29324f0..505ae0dad7 100644 --- a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IExecutionContextProvider } from './IExecutionContext'; +import type { IAsyncContextLoader, IContextLoader, IExecutionContextProvider, ISyncContextLoader } from './IExecutionContext'; import type { IExecutorHandler } from './IExecutorHandler'; export type ExecutorDataFilter = (data: T, contexts: IExecutionContextProvider) => boolean; @@ -25,9 +25,19 @@ export interface IExecutorHandlersCollection>; readonly chain: Array>; readonly collections: Array>; + readonly contextCreators: Map, IContextLoader>; setInitialDataGetter(getter: () => T): this; + addContextCreator( + context: ISyncContextLoader, + creator: ISyncContextLoader + ): this; + addContextCreator( + context: IAsyncContextLoader, + creator: IAsyncContextLoader + ): this; + before: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; next: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; addCollection: (collection: IExecutorHandlersCollection) => this; diff --git a/webapp/packages/core-executor/src/SyncExecutor.ts b/webapp/packages/core-executor/src/SyncExecutor.ts index 37b62e8bd8..eccd4e4143 100644 --- a/webapp/packages/core-executor/src/SyncExecutor.ts +++ b/webapp/packages/core-executor/src/SyncExecutor.ts @@ -9,6 +9,7 @@ import { flat } from '@cloudbeaver/core-utils'; import { ExecutionContext } from './ExecutionContext'; +import { executionExceptionContext } from './executionExceptionContext'; import { ExecutorHandlersCollection } from './ExecutorHandlersCollection'; import { ExecutorInterrupter, IExecutorInterrupter } from './ExecutorInterrupter'; import type { IExecutionContext, IExecutionContextProvider } from './IExecutionContext'; @@ -55,9 +56,16 @@ export class SyncExecutor extends ExecutorHandlersCollection implem context: IExecutionContext, scoped: Array> ): IExecutionContextProvider { - const interrupter = context.getContext(ExecutorInterrupter.interruptContext); scoped = [...collection.collections, ...scoped]; + context.addContextCreators(collection.contextCreators as any); + + for (const scope of scoped) { + context.addContextCreators(scope.contextCreators as any); + } + + const interrupter = context.getContext(ExecutorInterrupter.interruptContext); + this.executeChain(collection, data, context, 'before'); for (const scope of scoped) { @@ -70,6 +78,10 @@ export class SyncExecutor extends ExecutorHandlersCollection implem for (const scope of scoped) { this.executeHandlers(data, context, scope.handlers, interrupter); } + } catch (exception: any) { + const exceptionContext = context.getContext(executionExceptionContext); + exceptionContext.setException(exception); + throw exception; } finally { this.executeHandlers(data, context, collection.postHandlers); @@ -144,6 +156,8 @@ export class SyncExecutor extends ExecutorHandlersCollection implem const data = this.initialDataGetter(); const context = new ExecutionContext(data); + context.addContextCreators(this.contextCreators as any); + try { handler(data, context); } finally { diff --git a/webapp/packages/core-executor/src/executionExceptionContext.ts b/webapp/packages/core-executor/src/executionExceptionContext.ts new file mode 100644 index 0000000000..819e73a140 --- /dev/null +++ b/webapp/packages/core-executor/src/executionExceptionContext.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +export function executionExceptionContext() { + return { + exception: null as null | Error, + setException(exception: Error) { + this.exception = exception; + }, + }; +} \ No newline at end of file diff --git a/webapp/packages/core-executor/src/index.ts b/webapp/packages/core-executor/src/index.ts index 22f248d2d2..d9aeff7deb 100644 --- a/webapp/packages/core-executor/src/index.ts +++ b/webapp/packages/core-executor/src/index.ts @@ -3,6 +3,7 @@ export * from './TaskScheduler/ITask'; export * from './TaskScheduler/Task'; export * from './TaskScheduler/TaskScheduler'; export * from './ExecutionContext'; +export * from './executionExceptionContext'; export * from './Executor'; export * from './ExecutorHandlersCollection'; export * from './ExecutorInterrupter'; diff --git a/webapp/packages/core-projects/src/ProjectInfoResource.ts b/webapp/packages/core-projects/src/ProjectInfoResource.ts index 3918b39dca..7fb80adf94 100644 --- a/webapp/packages/core-projects/src/ProjectInfoResource.ts +++ b/webapp/packages/core-projects/src/ProjectInfoResource.ts @@ -10,6 +10,7 @@ import { runInAction } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; +import { SharedProjectsResource } from '@cloudbeaver/core-resource-manager'; import { GraphQLService, ProjectInfo as SchemaProjectInfo, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList } from '@cloudbeaver/core-sdk'; export type ProjectInfo = SchemaProjectInfo; @@ -18,11 +19,15 @@ export type ProjectInfo = SchemaProjectInfo; export class ProjectInfoResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, + private readonly sharedProjectsResource: SharedProjectsResource, private readonly userInfoResource: UserInfoResource, ) { super([]); this.sync(this.userInfoResource); + this.sharedProjectsResource.onDataOutdated.addHandler(this.markOutdated.bind(this)); + this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated()); + this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated()); this.userInfoResource.onUserChange.addPostHandler(() => { this.clear(); }); From b634b236df415f031d853c74f82ab1458d0a8658 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 9 Sep 2022 15:28:57 +0300 Subject: [PATCH 10/22] CB-2344 feat: display projects in resource manager --- .../ElementsTree/NavigationNodeComponent.ts | 1 + .../NavigationTreeNode/NavigationNode.tsx | 1 + .../NavigationNodeProjectControl.tsx | 8 +- ...navigationTreeProjectsRendererRenderer.tsx | 9 +- .../plugin-navigation-tree/src/index.ts | 7 +- .../NavigationNodeProjectControl.tsx | 105 ++++++++++++++++++ .../navigationTreeProjectFilter.ts | 34 ++++++ ...navigationTreeProjectsExpandStateGetter.ts | 31 ++++++ ...navigationTreeProjectsRendererRenderer.tsx | 82 ++++++++++++++ .../src/Tree/ResourceManagerTree.tsx | 29 ++++- 10 files changed, 299 insertions(+), 8 deletions(-) create mode 100644 webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx create mode 100644 webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectFilter.ts create mode 100644 webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts create mode 100644 webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts index 0b07f34691..23ae9ce148 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationNodeComponent.ts @@ -26,6 +26,7 @@ export type NavTreeControlProps = { dndPlaceholder?: boolean; expanded?: boolean; className?: string; + style?: ComponentStyle; ref?: React.Ref | undefined; }; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx index d44dbb20ce..8cc356dfab 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx @@ -113,6 +113,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat {(expanded || externalExpanded) && ( 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 f9a2977de1..c13380196b 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -59,6 +59,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer + {editing ? ( setEditing(false)} /> diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx index f6839cb2e5..386aab913d 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx @@ -14,7 +14,6 @@ import { Translate } from '@cloudbeaver/core-localization'; import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects'; -import { useChildren } from '../../NodesManager/useChildren'; import { useNode } from '../../NodesManager/useNode'; import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent'; import { NavigationNodeRenderer } from '../ElementsTree/NavigationTreeNode/NavigationNodeRenderer'; @@ -28,6 +27,14 @@ const nestedStyles = css` NavigationNodeNested { padding-left: 8px !important; } + TreeNode:only-child { + & Control { + display: none; + } + & NavigationNodeNested { + padding-left: 0 !important; + } + } `; export function navigationTreeProjectsRendererRenderer( diff --git a/webapp/packages/plugin-navigation-tree/src/index.ts b/webapp/packages/plugin-navigation-tree/src/index.ts index a4f4e4bc10..010ece5893 100644 --- a/webapp/packages/plugin-navigation-tree/src/index.ts +++ b/webapp/packages/plugin-navigation-tree/src/index.ts @@ -1,11 +1,14 @@ export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings'; export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT'; export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/KEY_BINDING_ENABLE_FILTER'; +export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor'; +export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl'; +export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu'; +export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeRenderer'; export * from './NavigationTree/ElementsTree/ACTION_LINK_OBJECT'; export * from './NavigationTree/ElementsTree/DATA_CONTEXT_ELEMENTS_TREE'; export * from './NavigationTree/ElementsTree/ElementsTree'; export * from './NavigationTree/ElementsTree/ElementsTreeContext'; -export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl'; export * from './NavigationTree/ElementsTree/KEY_BINDING_COLLAPSE_ALL'; export * from './NavigationTree/ElementsTree/KEY_BINDING_LINK_OBJECT'; export * from './NavigationTree/ElementsTree/MENU_NAV_TREE'; @@ -13,8 +16,8 @@ export * from './NavigationTree/ElementsTree/NavigationNodeComponent'; export * from './NavigationTree/ElementsTree/NavNodeFilterCompareFn'; export * from './NavigationTree/ElementsTree/useElementsTree'; export * from './NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS'; -export * from './NavigationTree/NavigationTreeBootstrap'; export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService'; +export * from './NavigationTree/NavigationTreeBootstrap'; export * from './NavigationTree/NavigationTreeService'; export * from './NavigationTree/NavigationTree'; export * from './NodesManager/NavNodeView/IFolderTransform'; diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx new file mode 100644 index 0000000000..533cf52d35 --- /dev/null +++ b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -0,0 +1,105 @@ +/* + * 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 { observer } from 'mobx-react-lite'; +import React, { forwardRef, useContext, useState } from 'react'; +import styled, { css, use } from 'reshadow'; + +import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; +import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree'; +import { NavigationNodeEditor, NavTreeControlComponent, NavTreeControlProps, TreeNodeMenu } from '@cloudbeaver/plugin-navigation-tree'; + + +const styles = css` + TreeNodeControl { + transition: opacity 0.3s ease; + opacity: 1; + + &[|outdated] { + opacity: 0.5; + } + } + TreeNodeControl:hover > portal, + TreeNodeControl:global([aria-selected=true]) > portal, + portal:focus-within { + visibility: visible; + } + portal { + position: relative; + } + TreeNodeName { + composes: theme-text-text-hint-on-light theme-typography--caption from global; + height: 100%; + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + } + portal { + box-sizing: border-box; + margin-left: auto !important; + margin-right: 16px !important; + visibility: hidden; + } + name-box { + overflow: hidden; + text-overflow: ellipsis; + } +`; + +export const NavigationNodeProjectControl: NavTreeControlComponent = observer(forwardRef(function NavigationNodeProjectControl({ + node, + dndElement, + dndPlaceholder, + className, +}, ref) { + const treeNodeContext = useContext(TreeNodeContext); + const navNodeInfoResource = useService(NavNodeInfoResource); + const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading); + const selected = treeNodeContext.selected; + + const [editing, setEditing] = useState(false); + + const nodeActions = useObjectRef({ + rename: () => { + setEditing(true); + }, + }); + + function handlePortalClick(event: React.MouseEvent) { + EventContext.set(event, EventStopPropagationFlag); + treeNodeContext.select(); + } + + function onClickHandler(event: React.MouseEvent) { + treeNodeContext.select(event.ctrlKey || event.metaKey); + } + + return styled(TREE_NODE_STYLES, styles)( + + + {editing ? ( + setEditing(false)} /> + ) : ( + {node.name} + )} + + {!editing && !dndPlaceholder && ( + + + + )} + + ); +})); diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectFilter.ts b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectFilter.ts new file mode 100644 index 0000000000..65a7e504f8 --- /dev/null +++ b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectFilter.ts @@ -0,0 +1,34 @@ +/* + * 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 { NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; +import type { IElementsTreeFilter } from '@cloudbeaver/plugin-navigation-tree'; + +import { NAV_NODE_TYPE_RM_PROJECT } from '../../NAV_NODE_TYPE_RM_PROJECT'; +import { RESOURCES_NODE_PATH } from '../../RESOURCES_NODE_PATH'; + +export function navigationTreeProjectFilter( + navNodeInfoResource: NavNodeInfoResource, + navTreeResource: NavTreeResource, +): IElementsTreeFilter { + return (filter: string, node: NavNode, children: string[]) => { + if (node.id !== RESOURCES_NODE_PATH) { + return children; + } + + const nodes = navNodeInfoResource + .get(resourceKeyList(children)) + .filter((node => node !== undefined) as (node: NavNode | undefined) => node is NavNode) + .filter(node => node.nodeType !== NAV_NODE_TYPE_RM_PROJECT || navTreeResource.get(node.id)?.length) + .map(node => node.id); + + return nodes; + }; +} diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts new file mode 100644 index 0000000000..b786b9e2d2 --- /dev/null +++ b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts @@ -0,0 +1,31 @@ +/* + * 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 { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import type { IElementsTreeNodeExpandInfoGetter } from '@cloudbeaver/plugin-navigation-tree'; + +import { NAV_NODE_TYPE_RM_PROJECT } from '../../NAV_NODE_TYPE_RM_PROJECT'; + + +export function navigationTreeProjectsExpandStateGetter( + navNodeInfoResource: NavNodeInfoResource +): IElementsTreeNodeExpandInfoGetter { + + return nodeId => { + const node = navNodeInfoResource.get(nodeId); + + if (node?.nodeType !== NAV_NODE_TYPE_RM_PROJECT) { + return null; + } + + return { + expanded: true, + expandable: false, + }; + }; +} \ No newline at end of file diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx new file mode 100644 index 0000000000..d1ceff13e0 --- /dev/null +++ b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx @@ -0,0 +1,82 @@ +/* + * 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 { observer } from 'mobx-react-lite'; +import styled, { css } from 'reshadow'; + +import { TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; +import { Translate } from '@cloudbeaver/core-localization'; +import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import { type IElementsTreeCustomRenderer, type NavigationNodeRendererComponent, useNode, NavigationNodeRenderer } from '@cloudbeaver/plugin-navigation-tree'; + +import { NAV_NODE_TYPE_RM_PROJECT } from '../../NAV_NODE_TYPE_RM_PROJECT'; +import { NavigationNodeProjectControl } from './NavigationNodeProjectControl'; + +const nestedStyles = css` + TreeNode { + margin-top: 8px; + } + NavigationNodeNested { + padding-left: 8px !important; + } + TreeNode:only-child { + & Control { + display: none; + } + & NavigationNodeNested { + padding-left: 0 !important; + } + } +`; + +export function navigationTreeProjectsRendererRenderer( + navNodeInfoResource: NavNodeInfoResource +): IElementsTreeCustomRenderer { + + return nodeId => { + const node = navNodeInfoResource.get(nodeId); + + if (node?.nodeType === NAV_NODE_TYPE_RM_PROJECT) { + return ProjectRenderer; + } + + return undefined; + }; +} + +const ProjectRenderer: NavigationNodeRendererComponent = observer(function ManageableGroup({ + nodeId, + path, + dragging, + component, + className, + expanded, +}) { + const { node } = useNode(nodeId); + + if (!node) { + return styled(TREE_NODE_STYLES)( + + + + ); + } + + return ( + + ); +}); diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx b/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx index a244bfe1c0..de81e95138 100644 --- a/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx +++ b/webapp/packages/plugin-resource-manager/src/Tree/ResourceManagerTree.tsx @@ -7,17 +7,21 @@ */ import { observer } from 'mobx-react-lite'; +import { useMemo } from 'react'; import styled, { css } from 'reshadow'; import { Loader, useDataResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { createPath } from '@cloudbeaver/core-utils'; +import { NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; import { CaptureView } from '@cloudbeaver/core-view'; -import { NavigationTreeService, ElementsTree, NavigationNodeControl } from '@cloudbeaver/plugin-navigation-tree'; +import { NavigationTreeService, ElementsTree } from '@cloudbeaver/plugin-navigation-tree'; import { ResourceProjectsResource } from '../ResourceProjectsResource'; import { RESOURCES_NODE_PATH } from '../RESOURCES_NODE_PATH'; +import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter'; +import { navigationTreeProjectsExpandStateGetter } from './ProjectsRenderer/navigationTreeProjectsExpandStateGetter'; +import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navigationTreeProjectsRendererRenderer'; const styles = css` CaptureView { @@ -54,18 +58,35 @@ const styles = css` export const ResourceManagerTree = observer(function ResourceManagerTree() { const translate = useTranslate(); + const navNodeInfoResource = useService(NavNodeInfoResource); + const navTreeResource = useService(NavTreeResource); const navTreeService = useService(NavigationTreeService); const { resource } = useDataResource(ResourceManagerTree, ResourceProjectsResource, undefined); + const projectsRendererRenderer = useMemo( + () => navigationTreeProjectsRendererRenderer(navNodeInfoResource), + [navNodeInfoResource] + ); + const projectsExpandStateGetter = useMemo( + () => navigationTreeProjectsExpandStateGetter(navNodeInfoResource), + [navNodeInfoResource] + ); + const projectFilter = useMemo( + () => navigationTreeProjectFilter(navNodeInfoResource, navTreeResource), + [navNodeInfoResource, navTreeResource] + ); + return styled(styles)( styled(styles)(
From 22bb27da5c06565715bedb8345e9087fa0bbeb38 Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Fri, 9 Sep 2022 17:29:14 +0300 Subject: [PATCH 11/22] CB-2344 add created project to web session --- .../src/io/cloudbeaver/service/rm/impl/WebServiceRM.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 5fad3e0820..ed3403c1f0 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 @@ -128,7 +128,9 @@ public class WebServiceRM implements DBWServiceRM { @NotNull WebSession session, @NotNull String name, @Nullable String description ) throws DBWebException { try { - return getResourceController(session).createProject(name, description); + RMProject rmProject = getResourceController(session).createProject(name, description); + session.createVirtualProject(rmProject); + return rmProject; } catch (DBException e) { throw new DBWebException("Error creating project", e); } From 0324360feaa2779ab737db458901439626801792 Mon Sep 17 00:00:00 2001 From: Konstantin Vasilev Date: Fri, 9 Sep 2022 17:31:08 +0300 Subject: [PATCH 12/22] CB-2482 feat(plugin-connections) Add project selector to AWS dialog --- .../core-blocks/src/FormControls/Combobox.tsx | 13 ++++++++++++- .../src/ConnectionForm/Options/ProjectSelect.tsx | 14 +++++++++++--- webapp/packages/plugin-connections/src/index.ts | 1 + 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index b82dbb201f..52962a630d 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -24,6 +24,15 @@ import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormCon import { FormContext } from './FormContext'; const styles = css` + field[|inline] { + display: flex; + align-items: center; + + & field-label { + padding-right: 8px; + padding-bottom: 0; + } + } field input { margin: 0; } @@ -137,6 +146,7 @@ type BaseProps = Omit, iconSelector?: (item: TValue) => string | React.ReactElement | undefined; isDisabled?: (item: TValue) => boolean; onSwitch?: (state: boolean) => void; + inline?: boolean; }; type ControlledProps = BaseProps & { @@ -174,6 +184,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ searchable, readOnly, disabled, + inline, keySelector = v => v, valueSelector = v => v, iconSelector, @@ -337,7 +348,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ } return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles))( - + {children && {children}{rest.required && ' *'}} {(icon || loading) && ( diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx index d57ab1917e..884be717be 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx @@ -5,20 +5,20 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ - import type React from 'react'; +import { useEffect } from 'react'; import { Combobox, useMapResource } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { Project, ProjectsResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; - interface Props { value: string; onChange: (value: string) => void; readOnly?: boolean; disabled?: boolean; + inline?: boolean; } export function ProjectSelect(props: Props) { @@ -27,6 +27,14 @@ export function ProjectSelect(props: Props) { const projectsLoader = useMapResource(ProjectSelect, ProjectsResource, CachedMapAllKey); const projects = projectsLoader.data as Project[]; + const possibleOptions = projects.filter(project => project.canCreateConnections); + + useEffect(() => { + if (!props.value && possibleOptions[0]) { + props.onChange(possibleOptions[0].id); + } + }, [projects]); + function handleProjectSelect(projectId: string) { const project = projectsLoader.resource.get(projectId); @@ -35,7 +43,6 @@ export function ProjectSelect(props: Props) { } } - const possibleOptions = projects.filter(project => project.canCreateConnections); return ( 10} disabled={props.disabled} loading={projectsLoader.isLoading()} + inline={props.inline} tiny fill onSelect={handleProjectSelect} diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index 0dbc64dcd4..75bb686130 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -2,6 +2,7 @@ import { connectionPlugin } from './manifest'; export * from './ConnectionAuthentication/IConnectionAuthenticationConfig'; export * from './ConnectionAuthentication/ConnectionAuthenticationForm'; export * from './ConnectionForm/Options/ConnectionOptionsTabService'; +export * from './ConnectionForm/Options/ProjectSelect'; export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService'; export * from './ConnectionForm/SSH/ConnectionSSHTabService'; export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; From f3ab06a2a3194537998c1828cce91bf32467c987 Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Fri, 9 Sep 2022 18:01:55 +0300 Subject: [PATCH 13/22] CB-2344 validate that project already exist --- .../model/rm/local/LocalResourceController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 5dc79c8373..2b8aca87b9 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 @@ -218,10 +218,14 @@ public class LocalResourceController implements RMController { } validateResourcePath(name); RMProject project; - project = makeProjectFromPath(sharedProjectsPath.resolve(name), Set.of(), RMProject.Type.SHARED, false); - if (project == null) { + var projectPath = sharedProjectsPath.resolve(name); + if (Files.exists(projectPath)) { throw new DBException("Project '" + name + "' already exists"); } + project = makeProjectFromPath(projectPath, Set.of(), RMProject.Type.SHARED, false); + if (project == null) { + throw new DBException("Project '" + name + "' not created"); + } try { Files.createDirectories(getProjectPath(project.getId())); return project; From 1280fd8b20c0b2ac799e641371cb993ee8666563 Mon Sep 17 00:00:00 2001 From: Aleksandr Skoblikov Date: Fri, 9 Sep 2022 18:06:59 +0300 Subject: [PATCH 14/22] CB-2344 use project name instead of project id in navigator --- .../io/cloudbeaver/model/rm/DBNResourceManagerProject.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5d638eab22..efec7ace4c 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 @@ -53,7 +53,7 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode { @NotNull @Override public String getName() { - return project.getId(); + return project.getDisplayName(); } @Override @@ -102,7 +102,7 @@ public class DBNResourceManagerProject extends DBNAbstractResourceManagerNode { @Override public String getNodeItemPath() { - return getParentNode().getNodeItemPath() + "/" + getName(); + return getParentNode().getNodeItemPath() + "/" + project.getId(); } @Override From 802e1a2a11af5e79aa22f4032ca04c04821b5bc4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 9 Sep 2022 18:35:19 +0300 Subject: [PATCH 15/22] CB-2344 feat: project selection in save script dialog --- .../ConnectionForm/Options/ProjectSelect.tsx | 5 +-- .../src/ResourceProjectsResource.ts | 6 ++++ .../src/SaveScriptDialog.tsx | 34 +++++++++++++------ .../src/PluginBootstrap.ts | 4 +-- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx index 0eb22f1e31..f0f08d7a55 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProjectSelect.tsx @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; import type React from 'react'; import { Combobox, useMapResource } from '@cloudbeaver/core-blocks'; @@ -21,7 +22,7 @@ interface Props { disabled?: boolean; } -export function ProjectSelect(props: Props) { +export const ProjectSelect = observer(function ProjectSelect(props: Props) { const translate = useTranslate(); const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey); @@ -57,4 +58,4 @@ export function ProjectSelect(props: Props) { {translate('connections_connection_project')} ); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts b/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts index 58f8381c0d..5b74ed68b5 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceProjectsResource.ts @@ -10,6 +10,7 @@ import { computed, makeObservable } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; +import { SharedProjectsResource } from '@cloudbeaver/core-resource-manager'; import { GraphQLService, CachedDataResource, RmProject } from '@cloudbeaver/core-sdk'; export type Project = Omit; @@ -23,6 +24,7 @@ export class ResourceProjectsResource extends CachedDataResource { constructor( private readonly graphQLService: GraphQLService, private readonly userInfoResource: UserInfoResource, + private readonly sharedProjectsResource: SharedProjectsResource, ) { super([]); @@ -31,6 +33,10 @@ export class ResourceProjectsResource extends CachedDataResource { this.markOutdated(); }); + this.sharedProjectsResource.onDataOutdated.addHandler(() => this.markOutdated()); + this.sharedProjectsResource.onItemAdd.addHandler(() => this.markOutdated()); + this.sharedProjectsResource.onItemDelete.addHandler(() => this.markOutdated()); + makeObservable(this, { userProject: computed, }); diff --git a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx index b30d2e9597..ab18bf5d9b 100644 --- a/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx +++ b/webapp/packages/plugin-resource-manager/src/SaveScriptDialog.tsx @@ -11,9 +11,12 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, useFocus, useObservableRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs'; import { Translate, useTranslate } from '@cloudbeaver/core-localization'; +import { ProjectsService, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects'; import { useStyles } from '@cloudbeaver/core-theming'; +import { ProjectSelect } from '@cloudbeaver/plugin-connections/src/ConnectionForm/Options/ProjectSelect'; const style = css` fill { @@ -25,8 +28,12 @@ interface Payload { defaultScriptName?: string; } -interface State { - value: string; +export interface ISaveScriptDialogResult { + name: string; + projectId: string; +} + +interface State extends ISaveScriptDialogResult { errorMessage: string | null; validate: () => void; submit: () => Promise; @@ -34,7 +41,7 @@ interface State { const regex = /^(?!\.)[\p{L}\w\-$.\s()@]+$/u; -export const SaveScriptDialog: DialogComponent = observer(function SaveScriptDialog({ +export const SaveScriptDialog: DialogComponent = observer(function SaveScriptDialog({ payload, resolveDialog, rejectDialog, @@ -42,14 +49,16 @@ export const SaveScriptDialog: DialogComponent = observer(funct }) { const translate = useTranslate(); const [focusedRef] = useFocus({ focusFirstChild: true }); + const projectsService = useService(ProjectsService); const state = useObservableRef(() => ({ - value: payload.defaultScriptName ?? '', + name: payload.defaultScriptName ?? '', + projectId: projectsService.activeProject?.id ?? PROJECT_GLOBAL_ID, errorMessage: null, validate() { this.errorMessage = null; - const valid = regex.test(this.value.trim()); + const valid = regex.test(this.name.trim()); if (!valid) { this.errorMessage = translate('plugin_resource_manager_script_name_invalid_characters_message'); @@ -59,11 +68,12 @@ export const SaveScriptDialog: DialogComponent = observer(funct this.validate(); if (!this.errorMessage) { - resolveDialog(this.value); + resolveDialog(this); } }, }), { - value: observable.ref, + name: observable.ref, + projectId: observable.ref, errorMessage: observable.ref, validate: action.bound, submit: action.bound, @@ -89,7 +99,7 @@ export const SaveScriptDialog: DialogComponent = observer(funct