Merge pull request #1144 from dbeaver/CB-2406-projects-permission-managment

CB-2406 rm project permissions api
This commit is contained in:
Alexander Skoblikov
2022-09-20 15:35:15 +03:00
committed by GitHub
120 changed files with 1708 additions and 434 deletions
@@ -29,6 +29,8 @@ Export-Package: io.cloudbeaver,
io.cloudbeaver.registry,
io.cloudbeaver.server,
io.cloudbeaver.service,
io.cloudbeaver.service.admin,
io.cloudbeaver.service.security,
io.cloudbeaver.service.sql,
io.cloudbeaver.utils
Automatic-Module-Name: io.cloudbeaver.model
@@ -346,7 +346,7 @@ public class WebConnectionInfo {
@Property
public boolean isCanEdit() {
return hasProjectPermission(RMProjectPermission.CONNECTIONS_EDIT);
return hasProjectPermission(RMProjectPermission.DATA_SOURCES_EDIT);
}
@Property
@@ -18,6 +18,7 @@ package io.cloudbeaver.model;
import io.cloudbeaver.VirtualProjectImpl;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.security.SMUtils;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.rm.RMProjectPermission;
@@ -55,16 +56,27 @@ public class WebProjectInfo {
}
@Property
public boolean isCanCreateConnections() {
return hasRmPermission(RMProjectPermission.CONNECTIONS_EDIT);
public boolean isCanEditDataSources() {
return hasDataSourcePermission(RMProjectPermission.DATA_SOURCES_EDIT);
}
@Property
public boolean isCanViewConnections() {
return hasRmPermission(RMProjectPermission.CONNECTIONS_VIEW);
public boolean isCanViewDataSources() {
return hasDataSourcePermission(RMProjectPermission.DATA_SOURCES_VIEW);
}
private boolean hasRmPermission(RMProjectPermission permission) {
return project.getRmProject().getProjectPermissions().contains(permission.getPermissionId());
@Property
public boolean isCanEditResources() {
return hasDataSourcePermission(RMProjectPermission.RESOURCE_EDIT);
}
@Property
public boolean isCanViewResources() {
return hasDataSourcePermission(RMProjectPermission.RESOURCE_VIEW);
}
private boolean hasDataSourcePermission(RMProjectPermission permission) {
return SMUtils.isRMAdmin(session)
|| project.getRmProject().getProjectPermissions().contains(permission.getPermissionId());
}
}
@@ -49,7 +49,7 @@ public class RMUtils {
RMProject project = new RMProject("anonymous");
project.setId("anonymous");
project.setType(RMProject.Type.USER);
project.setProjectPermissions(RMProjectPermission.CONNECTIONS_EDIT.getAllPermissions());
project.setProjectPermissions(RMProjectPermission.DATA_SOURCES_EDIT.getAllPermissions());
return project;
}
}
@@ -19,6 +19,7 @@ package io.cloudbeaver.model.rm.local;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.VirtualProjectImpl;
import io.cloudbeaver.model.rm.RMUtils;
import io.cloudbeaver.service.security.SMUtils;
import io.cloudbeaver.service.sql.WebSQLConstants;
import io.cloudbeaver.utils.WebAppUtils;
import org.eclipse.core.runtime.IPath;
@@ -160,7 +161,7 @@ public class LocalResourceController implements RMController {
return accessibleSharedProjects
.stream()
.map(projectPermission -> makeProjectFromPath(
sharedProjectsPath.resolve(projectPermission.getObjectId()),
sharedProjectsPath.resolve(parseProjectName(projectPermission.getObjectId()).getName()),
projectPermission.getPermissions().stream().map(RMProjectPermission::fromPermission).collect(Collectors.toSet()),
RMProject.Type.SHARED, true)
)
@@ -173,20 +174,25 @@ public class LocalResourceController implements RMController {
switch (projectType) {
case GLOBAL:
return credentialsProvider.hasPermission(DBWConstants.PERMISSION_ADMIN)
? Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.CONNECTIONS_EDIT)
: Set.of(RMProjectPermission.RESOURCE_VIEW, RMProjectPermission.CONNECTIONS_VIEW);
return SMUtils.isRMAdmin(credentialsProvider)
? Set.of(RMProjectPermission.PROJECT_ADMIN)
: Set.of(RMProjectPermission.RESOURCE_VIEW, RMProjectPermission.DATA_SOURCES_VIEW);
case SHARED:
if(projectId == null) {
if (SMUtils.isRMAdmin(credentialsProvider)) {
return Set.of(RMProjectPermission.PROJECT_ADMIN);
}
if (projectId == null) {
throw new DBException("Project id required");
}
return smController.getObjectPermissions(activeUserCreds.getUserId(), projectId, SMObjects.PROJECT)
.getPermissions()
.stream()
.map(RMProjectPermission::fromPermission)
.collect(Collectors.toSet());
case USER:
return Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.CONNECTIONS_EDIT);
return Set.of(RMProjectPermission.RESOURCE_EDIT, RMProjectPermission.DATA_SOURCES_EDIT);
default:
throw new DBException("Unknown project type:" + projectType);
}
@@ -199,8 +205,17 @@ public class LocalResourceController implements RMController {
if (!Files.exists(sharedProjectsPath)) {
return new RMProject[0];
}
return Files.list(sharedProjectsPath)
.map((Path path) -> makeProjectFromPath(path, Set.of(), RMProject.Type.SHARED, false))
var projects = new ArrayList<RMProject>();
var allPaths = Files.list(sharedProjectsPath).collect(Collectors.toList());
for (Path path : allPaths) {
var projectPerms = getProjectPermissions(
makeProjectIdFromPath(path, RMProject.Type.SHARED),
RMProject.Type.SHARED
);
var rmProject = makeProjectFromPath(path, projectPerms, RMProject.Type.SHARED, false);
projects.add(rmProject);
}
return projects.stream()
.filter(Objects::nonNull)
.toArray(RMProject[]::new);
} catch (IOException e) {
@@ -244,6 +259,7 @@ public class LocalResourceController implements RMController {
}
try {
CommonUtils.deleteDirectory(targetPath);
smController.deleteAllObjectPermissions(projectId, SMObjects.PROJECT);
} catch (IOException e) {
throw new DBException("Error deleting project '" + project.getName() + "'", e);
}
@@ -525,11 +541,16 @@ public class LocalResourceController implements RMController {
}
private String makeProjectIdFromPath(Path path, RMProject.Type type) {
String projectName = path.getFileName().toString();
return type.getPrefix() + "_" + projectName;
}
private RMProject makeProjectFromId(String projectId, boolean loadPermissions) throws DBException {
var projectName = parseProjectName(projectId);
var projectPath = getProjectPath(projectId);
Set<RMProjectPermission> permissions = Set.of();
if(loadPermissions && credentialsProvider.getActiveUserCredentials() != null) {
if (loadPermissions && credentialsProvider.getActiveUserCredentials() != null) {
permissions = getProjectPermissions(projectId, projectName.getType());
}
return makeProjectFromPath(projectPath, permissions, projectName.getType(), false);
@@ -555,7 +576,7 @@ public class LocalResourceController implements RMController {
RMProject project = new RMProject();
String projectName = path.getFileName().toString();
project.setName(projectName);
project.setId(type.getPrefix() + "_" + projectName);
project.setId(makeProjectIdFromPath(path, type));
project.setType(type);
project.setProjectPermissions(allProjectPermissions);
if (Files.exists(path)) {
@@ -750,10 +771,9 @@ public class LocalResourceController implements RMController {
return new RMProjectName(prefix, name);
}
public static boolean isShared(String projectId) {
public static boolean isGlobalProject(String projectId) {
RMProjectName rmProjectName = parseProjectName(projectId);
return RMProject.Type.SHARED.getPrefix().equals(rmProjectName.getPrefix()) ||
RMProject.Type.GLOBAL.getPrefix().equals(rmProjectName.getPrefix());
return RMProject.Type.GLOBAL.getPrefix().equals(rmProjectName.getPrefix());
}
}
@@ -332,7 +332,7 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
RMProject[] rmProjects = controller.listAccessibleProjects();
for (RMProject project : rmProjects) {
VirtualProjectImpl virtualProject = createVirtualProject(project);
if (!virtualProject.getRmProject().getProjectPermissions().contains(RMProjectPermission.CONNECTIONS_EDIT.getPermissionId())) {
if (!virtualProject.getRmProject().getProjectPermissions().contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId())) {
// Projects for which user don't have edit permission can't be saved. So mark the as in memory
virtualProject.setInMemory(true);
}
@@ -349,7 +349,9 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
public VirtualProjectImpl createVirtualProject(RMProject project) {
// Do not filter data sources from user project
DataSourceFilter filter = project.getType() == RMProject.Type.USER ? x -> true : this::isDataSourceAccessible;
DataSourceFilter filter = project.getType() == RMProject.Type.GLOBAL
? this::isDataSourceAccessible
: x -> true;
VirtualProjectImpl sessionProject = application.createProjectImpl(
project,
sessionAuthContext,
@@ -0,0 +1,37 @@
package io.cloudbeaver.service.security;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.registry.WebPermissionDescriptor;
import io.cloudbeaver.registry.WebServiceDescriptor;
import io.cloudbeaver.registry.WebServiceRegistry;
import io.cloudbeaver.service.admin.AdminPermissionInfo;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.auth.SMCredentialsProvider;
import org.jkiss.dbeaver.model.rm.RMConstants;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SMUtils {
public static boolean isAdmin(SMCredentialsProvider webSession) {
return webSession.hasPermission(DBWConstants.PERMISSION_ADMIN);
}
public static boolean isRMAdmin(SMCredentialsProvider webSession) {
return isAdmin(webSession) || webSession.hasPermission(RMConstants.PERMISSION_RM_ADMIN);
}
public static List<AdminPermissionInfo> findPermissions(@NotNull String permissionsScope) {
List<AdminPermissionInfo> permissionInfos = new ArrayList<>();
for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) {
for (WebPermissionDescriptor pd : wsd.getPermissions()) {
if (permissionsScope.equals(pd.getScope())) {
permissionInfos.add(new AdminPermissionInfo(pd));
}
}
}
permissionInfos.sort(Comparator.comparing(AdminPermissionInfo::getLabel));
return permissionInfos;
}
}
@@ -370,8 +370,10 @@ type ProjectInfo {
id: String!
name: String!
description: String
canCreateConnections: Boolean!
canViewConnections: Boolean!
canEditDataSources: Boolean!
canViewDataSources: Boolean!
canEditResources: Boolean!
canViewResources: Boolean!
}
type LogEntry {
@@ -182,7 +182,9 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
private void checkObjectActionPermissions(Method method, WebProjectAction objectAction, Object[] args) throws DBException {
WebSession webSession = findWebSession(env);
if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
return;
}
String[] requireProjectPermissions = objectAction.requireProjectPermissions();
if (requireProjectPermissions.length > 0) {
int objectIdArgumentIndex = -1;
@@ -111,32 +111,32 @@ public interface DBWServiceCore extends DBWService {
@Nullable List<WebNetworkHandlerConfigInput> networkCredentials,
@Nullable Boolean saveCredentials) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionInfo createConnection(
@NotNull WebSession webSession,
@Nullable @WebObjectId String projectId,
@NotNull WebConnectionConfig connectionConfig) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionInfo updateConnection(
@NotNull WebSession webSession,
@Nullable @WebObjectId String projectId,
@NotNull WebConnectionConfig connectionConfig) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
boolean deleteConnection(
@NotNull WebSession webSession,
@Nullable @WebObjectId String projectId,
@NotNull String connectionId) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionInfo createConnectionFromTemplate(
@NotNull WebSession webSession,
@Nullable @WebObjectId String projectId,
@NotNull String templateId,
@Nullable String connectionName) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionInfo copyConnectionFromNode(
@NotNull WebSession webSession,
@Nullable @WebObjectId String projectId,
@@ -162,14 +162,14 @@ public interface DBWServiceCore extends DBWService {
///////////////////////////////////////////
// Folders
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionFolderInfo createConnectionFolder(
@NotNull WebSession session,
@Nullable @WebObjectId String projectId,
@NotNull String parentNodePath,
@NotNull String newName) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionFolderInfo renameConnectionFolder(
@NotNull WebSession session,
@Nullable @WebObjectId
@@ -177,7 +177,7 @@ public interface DBWServiceCore extends DBWService {
@NotNull String folderPath,
@NotNull String newName) throws DBWebException;
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_CONNECTIONS_EDIT})
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
boolean deleteConnectionFolder(
@NotNull WebSession session,
@Nullable @WebObjectId String projectId,
@@ -34,7 +34,6 @@ import org.jkiss.dbeaver.model.struct.DBSEntity;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.model.struct.rdb.DBSProcedure;
import org.jkiss.dbeaver.registry.DataSourceFolder;
import org.jkiss.dbeaver.registry.DataSourceRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import java.util.ArrayList;
@@ -185,7 +184,7 @@ public class WebNavigatorNodeInfo {
return features.toArray(new String[0]);
}
VirtualProjectImpl project = session.getProjectById(node.getOwnerProject().getId());
if (!project.getRmProject().getProjectPermissions().contains(RMProjectPermission.CONNECTIONS_EDIT.getPermissionId())) {
if (!project.getRmProject().getProjectPermissions().contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId())) {
return features.toArray(new String[0]);
}
if (node instanceof DBNLocalFolder || node instanceof DBNResourceManagerResource) {
@@ -432,8 +432,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
private void checkProjectEditAccess(DBNNode node, WebSession session) throws DBException {
var project = session.getProjectById(node.getOwnerProject().getId());
if (project == null
|| !hasNodeEditPermission(node, project.getRmProject())
if (project == null || !hasNodeEditPermission(node, project.getRmProject())
) {
throw new DBException("Access denied");
}
@@ -442,7 +441,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
private boolean hasNodeEditPermission(DBNNode node, RMProject rmProject) {
var projectPermissions = rmProject.getProjectPermissions();
if (node instanceof DBNDataSource || node instanceof DBNLocalFolder) {
return projectPermissions.contains(RMProjectPermission.CONNECTIONS_EDIT.getPermissionId());
return projectPermissions.contains(RMProjectPermission.DATA_SOURCES_EDIT.getPermissionId());
} else if (node instanceof DBNAbstractResourceManagerNode) {
return projectPermissions.contains(RMProjectPermission.RESOURCE_EDIT.getPermissionId());
}
@@ -11,6 +11,17 @@ type AdminConnectionGrantInfo {
subjectType: AdminSubjectType!
}
type AdminObjectPermissions {
objectId: ID!
permissions: [String!]!
}
type AdminObjectGrantInfo {
subjectId: ID!
subjectType: AdminSubjectType!
objectPermissions: AdminObjectPermissions!
}
type AdminConnectionSearchInfo {
displayName: String!
host: String!
@@ -37,6 +37,7 @@ import io.cloudbeaver.server.CBConstants;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.service.DBWServiceServerConfigurator;
import io.cloudbeaver.service.admin.*;
import io.cloudbeaver.service.security.SMUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -118,18 +119,9 @@ public class WebServiceAdmin implements DBWServiceAdmin {
@Override
public List<AdminPermissionInfo> listPermissions(@NotNull WebSession webSession) throws DBWebException {
try {
List<AdminPermissionInfo> permissionInfos = new ArrayList<>();
for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) {
for (WebPermissionDescriptor pd : wsd.getPermissions()) {
if(SMConstants.SUBJECT_PERMISSION_SCOPE.equals(pd.getScope())) {
permissionInfos.add(new AdminPermissionInfo(pd));
}
}
}
permissionInfos.sort(Comparator.comparing(AdminPermissionInfo::getLabel));
return permissionInfos;
return SMUtils.findPermissions(SMConstants.SUBJECT_PERMISSION_SCOPE);
} catch (Exception e) {
throw new DBWebException("Error reading users", e);
throw new DBWebException("Error reading permissions", e);
}
}
@@ -8,6 +8,7 @@ Bundle-Release-Date: 20220926
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
Require-Bundle: io.cloudbeaver.server
Require-Bundle: io.cloudbeaver.server,
io.cloudbeaver.service.admin
Bundle-Localization: OSGI-INF/l10n/bundle
Automatic-Module-Name: io.cloudbeaver.service.rm
@@ -5,15 +5,16 @@
<extension point="io.cloudbeaver.service">
<service id="resource.manager" label="Resource management " description="Resource management services" class="io.cloudbeaver.service.rm.WebServiceBindingRM">
<permission id="project-resource-view" label="Project Resource View" description="Allow read only access to resources"
category="general" scope="project"/>
<permission id="project-resource-edit" label="Project Resource Edit" description="Allow write access to resources"
category="general" scope="project"/>
<permission id="project-resource-view" label="Resource View" description="Allow read only access to resources"
category="resources" scope="project"/>
<permission id="project-resource-edit" label="Resource Edit" description="Allow write access to resources"
category="resources" scope="project"/>
<permission id="project-connection-view" label="Project Connection View" description="Allow view connections in the project"
category="general" scope="project"/>
<permission id="project-connection-edit" label="Project Connections Edit"
description="Allow create/edit connections in the project" category="general" scope="project"/>
<permission id="project-datasource-view" label="Connection View" description="Allow view connections in the project"
category="datasources" scope="project"/>
<permission id="project-datasource-edit" label="Connection Edit"
description="Allow create/edit connections in the project"
category="data-sources" scope="project"/>
<!-- <permission id="project-admin" label="Project Admin" description="Allow manage access to the project" category="general"-->
<!-- scope="project"/>-->
@@ -17,6 +17,11 @@ type RMResource {
length: Int!
}
input RMProjectPermissions {
subjectId: String!
permissions: [String!]!
}
extend type Query {
# List accessible projects
@@ -26,7 +31,12 @@ extend type Query {
rmProject(projectId: String!): RMProject!
# List accessible projects
rmListProjectPermissions: [AdminPermissionInfo!]!
rmListProjectGrantedPermissions(projectId: String!): [AdminObjectGrantInfo!]!
rmListSubjectProjectsPermissionGrants(subjectId: String!): [AdminObjectGrantInfo!]!
rmListResources(
projectId: String!,
folder: String,
@@ -70,4 +80,7 @@ extend type Mutation {
description: String): RMProject!
rmDeleteProject(projectId: ID!): Boolean!
rmSetProjectPermissions(projectId: String!, permissions: [RMProjectPermissions!]!): Boolean!
}
@@ -17,15 +17,19 @@
package io.cloudbeaver.service.rm;
import io.cloudbeaver.*;
import io.cloudbeaver.model.WebProjectInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.DBWService;
import io.cloudbeaver.service.admin.AdminPermissionInfo;
import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.rm.RMConstants;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.model.rm.RMResource;
import org.jkiss.dbeaver.model.security.SMObjectPermissionsGrant;
import java.util.List;
/**
* Web service API
@@ -99,4 +103,30 @@ public interface DBWServiceRM extends DBWService {
boolean deleteProject(
@NotNull WebSession session,
@NotNull @WebObjectId String projectId) throws DBWebException;
@WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN})
List<AdminPermissionInfo> listProjectPermissions() throws DBWebException;
@WebProjectAction(
requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN
)
boolean setProjectPermissions(
@NotNull WebSession webSession,
@NotNull @WebObjectId String projectId,
@NotNull RMSubjectProjectPermissions projectPermissions
) throws DBWebException;
@WebProjectAction(
requireProjectPermissions = RMConstants.PERMISSION_PROJECT_ADMIN
)
List<SMObjectPermissionsGrant> listProjectGrantedPermissions(
@NotNull WebSession webSession,
@NotNull @WebObjectId String projectId
) throws DBWebException;
@WebAction(requirePermissions = {RMConstants.PERMISSION_RM_ADMIN})
List<SMObjectPermissionsGrant> listSubjectProjectsPermissionGrants(
@NotNull WebSession webSession,
@NotNull String subjectId
) throws DBWebException;
}
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException;
import io.cloudbeaver.service.DBWBindingContext;
import io.cloudbeaver.service.WebServiceBindingBase;
import io.cloudbeaver.service.rm.impl.WebServiceRM;
import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions;
import org.jkiss.utils.CommonUtils;
/**
@@ -53,6 +54,15 @@ public class WebServiceBindingRM extends WebServiceBindingBase<DBWServiceRM> {
env -> getService(env).readResourceAsString(getWebSession(env),
env.getArgument("projectId"),
env.getArgument("resourcePath")))
.dataFetcher("rmListProjectPermissions", env -> getService(env).listProjectPermissions())
.dataFetcher("rmListProjectGrantedPermissions", env -> getService(env).listProjectGrantedPermissions(
getWebSession(env),
env.getArgument("projectId")
))
.dataFetcher("rmListSubjectProjectsPermissionGrants", env -> getService(env).listSubjectProjectsPermissionGrants(
getWebSession(env),
env.getArgument("subjectId")
))
;
model.getMutationType()
.dataFetcher("rmCreateResource",
@@ -80,6 +90,11 @@ public class WebServiceBindingRM extends WebServiceBindingBase<DBWServiceRM> {
getWebSession(env),
getProjectReference(env)
))
.dataFetcher("rmSetProjectPermissions", env -> getService(env).setProjectPermissions(
getWebSession(env),
env.getArgument("projectId"),
new RMSubjectProjectPermissions(env.getArgument("permissions"))
))
;
}
@@ -17,9 +17,11 @@
package io.cloudbeaver.service.rm.impl;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.WebProjectInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.admin.AdminPermissionInfo;
import io.cloudbeaver.service.rm.DBWServiceRM;
import io.cloudbeaver.service.rm.model.RMSubjectProjectPermissions;
import io.cloudbeaver.service.security.SMUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -27,8 +29,12 @@ import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.rm.RMController;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.model.rm.RMResource;
import org.jkiss.dbeaver.model.security.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Web service implementation
@@ -148,6 +154,61 @@ public class WebServiceRM implements DBWServiceRM {
}
}
@Override
public List<AdminPermissionInfo> listProjectPermissions() throws DBWebException {
try {
return SMUtils.findPermissions(SMConstants.PROJECT_PERMISSION_SCOPE);
} catch (Exception e) {
throw new DBWebException("Error reading project permissions", e);
}
}
@Override
public boolean setProjectPermissions(@NotNull WebSession webSession,
@NotNull String projectId,
@NotNull RMSubjectProjectPermissions projectPermissions
) throws DBWebException {
try {
SMController sm = webSession.getSecurityController();
//TODO one 'setObjectPermissions' call
sm.deleteAllObjectPermissions(projectId, SMObjects.PROJECT);
for (Map.Entry<String, Set<String>> entry : projectPermissions.getSubjectPermissions().entrySet()) {
String subjectId = entry.getKey();
Set<String> permissions = entry.getValue();
sm.setObjectPermissions(
Set.of(projectId),
SMObjects.PROJECT,
Set.of(subjectId),
permissions,
webSession.getUserId()
);
}
return true;
} catch (Exception e) {
throw new DBWebException("Error granting project permissions", e);
}
}
@Override
public List<SMObjectPermissionsGrant> listProjectGrantedPermissions(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException {
SMController sm = webSession.getSecurityController();
try {
return sm.getObjectPermissionGrants(projectId, SMObjects.PROJECT);
} catch (DBException e) {
throw new DBWebException("Error reading project permission grants", e);
}
}
@Override
public List<SMObjectPermissionsGrant> listSubjectProjectsPermissionGrants(@NotNull WebSession webSession, @NotNull String subjectId) throws DBWebException {
try {
SMAdminController sm = webSession.getAdminSecurityController();
return sm.getSubjectObjectPermissionGrants(subjectId, SMObjects.PROJECT);
} catch (DBException e) {
throw new DBWebException("Error reading project permission grants", e);
}
}
private RMController getResourceController(WebSession webSession) {
return webSession.getRmController();
}
@@ -0,0 +1,21 @@
package io.cloudbeaver.service.rm.model;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import java.util.*;
public class RMSubjectProjectPermissions {
private final Map<String, Set<String>> subjectPermissions = new HashMap<>();
public RMSubjectProjectPermissions(List<Map<String, Object>> params) {
for (Map<String, Object> userPerms : params) {
String subjectId = JSONUtils.getString(userPerms, "subjectId");
Set<String> permissions = new HashSet<>(JSONUtils.getStringList(userPerms, "permissions"));
this.subjectPermissions.put(subjectId, permissions);
}
}
public Map<String, Set<String>> getSubjectPermissions() {
return subjectPermissions;
}
}
@@ -8,9 +8,10 @@
import { injectable } from '@cloudbeaver/core-di';
import { SessionDataResource } from '@cloudbeaver/core-root';
import { GraphQLService, AdminPermissionInfo, CachedMapResource, CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { GraphQLService, CachedMapResource, CachedMapAllKey, AdminPermissionInfoFragment, AdminObjectGrantInfoFragment } from '@cloudbeaver/core-sdk';
export type PermissionInfo = AdminPermissionInfo;
export type PermissionInfo = AdminPermissionInfoFragment;
export type AdminObjectGrantInfo = AdminObjectGrantInfoFragment;
@injectable()
export class PermissionsResource extends CachedMapResource<string, PermissionInfo> {
@@ -6,6 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { memo } from 'react';
import { Placeholder } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { TopNavBar } from '@cloudbeaver/plugin-top-app-bar';
@@ -13,7 +15,7 @@ import { TopNavBar } from '@cloudbeaver/plugin-top-app-bar';
import { AppScreenService } from './AppScreenService';
import { Main } from './Main';
export function AppScreen() {
export const AppScreen = memo(function AppScreen() {
const appScreenService = useService(AppScreenService);
return (
<>
@@ -22,4 +24,4 @@ export function AppScreen() {
<Main />
</>
);
}
});
@@ -9,12 +9,8 @@
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import {
splitStyles, Split, ResizerControls, Pane, ErrorBoundary, useMapResource, useSplitUserState
} from '@cloudbeaver/core-blocks';
import { ConnectionExecutionContextResource, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { splitStyles, Split, ResizerControls, Pane, ErrorBoundary, useSplitUserState } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import { SideBarPanel, SideBarPanelService } from '@cloudbeaver/core-ui';
import { NavigationTree } from '@cloudbeaver/plugin-navigation-tree';
@@ -42,12 +38,6 @@ export const Main = observer(function Main() {
const styles = useStyles(mainStyles, splitStyles);
const splitState = useSplitUserState('main');
// useMapResource(
// Main,
// ConnectionExecutionContextResource,
// CachedMapAllKey
// );
// useMapResource(Main, ConnectionInfoResource, CachedMapAllKey);
const activeBars = sideBarPanelService.tabsContainer.getDisplayed();
+9 -1
View File
@@ -109,6 +109,7 @@ export const Button = observer<ButtonProps>(function Button({
mod,
styles,
tag = 'button',
type = 'button',
disabled = false,
loading,
loader,
@@ -143,7 +144,14 @@ export const Button = observer<ButtonProps>(function Button({
const Button = tag;
return styled(useStyles(styles, buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))(
<Button {...rest} disabled={disabled} {...use({ loading })} className={className} onClick={state.click}>
<Button
{...rest}
type={type}
disabled={disabled}
{...use({ loading })}
className={className}
onClick={state.click}
>
<ripple />
{icon && <button-icon><Icon name={icon} viewBox={viewBox} /></button-icon>}
<button-label as='span'>{children}</button-label>
@@ -0,0 +1,25 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useEffect } from 'react';
import type { ILoadableState } from './ILoadableState';
export interface IAutoLoadable extends ILoadableState {
load: () => void;
}
export function useAutoLoad(state: IAutoLoadable, enabled = true) {
const load = enabled && !state.isLoaded();
useEffect(() => {
if (load) {
state.load();
}
}, [state, load]);
}
@@ -8,19 +8,34 @@
import { observer } from 'mobx-react-lite';
import type { PlaceholderContainer } from './PlaceholderContainer';
import type { PlaceholderContainer, PlaceholderElement } from './PlaceholderContainer';
type Props<T extends Record<string, any>> = T & {
container: PlaceholderContainer<T>;
elements?: PlaceholderElement<T>[];
};
export const Placeholder = observer(function Placeholder<T extends Record<string, any>>({
container,
elements: extraElements,
...rest
}: Props<T>) {
let elements = container.get();
if (extraElements) {
elements = [...elements, ...extraElements]
.sort((a, b) => {
if (a.order === b.order) {
return 0;
}
return (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER);
});
}
return (
<>
{container.get().map(({ id, component: Component }) => <Component key={id} {...(rest as any as T)} />)}
{elements.map(({ id, component: Component }) => <Component key={id} {...(rest as any as T)} />)}
</>
);
});
@@ -39,7 +39,7 @@ const styles = css`
`;
interface Props {
message?: string | null;
message?: string | string[] | null;
status?: ENotificationType;
exception?: Error | null;
onShowDetails?: () => void;
@@ -55,7 +55,12 @@ export const StatusMessage = observer<Props>(function StatusMessage({
}) {
const translate = useTranslate();
const errorDetails = useErrorDetails(exception);
message = (message ? translate(message) : message) ?? errorDetails.details?.name;
if (Array.isArray(message)) {
message = message.map(m => translate(m)).join(', ');
}
message = message ?? errorDetails.details?.message;
let icon = '/icons/info_icon.svg';
if (status === ENotificationType.Error || exception !== null) {
@@ -6,8 +6,8 @@
* you may not use this file except in compliance with the License.
*/
export function getSelectedItems(items: Map<any, boolean>): any[] {
const result: any[] = [];
export function getSelectedItems<T>(items: Map<T, boolean>): T[] {
const result: T[] = [];
for (const [key, value] of items) {
if (value) {
+1
View File
@@ -17,6 +17,7 @@ export * from './layout/BASE_LAYOUT_GRID_STYLES';
export * from './layout/TopAppBar';
export * from './Loader/Loader';
export * from './Loader/useAutoLoad';
export * from './Loader/ILoadableState';
export * from './Marks/ConnectionMark';
@@ -12,6 +12,7 @@ import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { CachedDataResource, CachedDataResourceContext, CachedDataResourceGetter, CachedDataResourceParam, CachedResourceData, CachedResourceIncludeArgs, isResourceKeyList } from '@cloudbeaver/core-sdk';
import { getComputed } from './getComputed';
import type { ILoadableState } from './Loader/ILoadableState';
import { useObjectRef } from './useObjectRef';
@@ -102,7 +103,7 @@ export function useDataResource<
async [loadFunctionName](refresh?: boolean) {
const { key, includes, loading, resource, actions, prevData } = this;
if (loading || actions?.active === false) {
if (loading) {
return;
}
@@ -195,16 +196,22 @@ export function useDataResource<
},
}));
useEffect(() => {
if ((!outdated && !refObj.firstRender) || refObj.key === null) {
return;
}
const canLoad = getComputed(() => (
result.exception === null
&& actions?.active !== false
&& !((!outdated && !refObj.firstRender) || refObj.key === null)
));
if (result.exception === null) {
useEffect(() => {
if (canLoad) {
(refObj as any)[loadFunctionName]();
}
});
// if (canLoad) {
// throw (refObj as any)[loadFunctionName]();
// }
return result;
}
@@ -22,6 +22,7 @@ interface FocusOptions {
interface IState<T extends HTMLElement> {
focus: boolean;
reference: T | null;
lastFocus: HTMLElement | null;
setRef: (ref: T | null) => void;
updateFocus: () => void;
focusFirstChild: () => void;
@@ -38,6 +39,7 @@ export function useFocus<T extends HTMLElement>({
() => ({
reference: null,
focus: false,
lastFocus: null,
setRef(ref: T | null) {
if (this.reference !== ref) {
this.reference = ref;
@@ -47,6 +49,13 @@ export function useFocus<T extends HTMLElement>({
},
updateFocus() {
if (this.reference) {
if (
document.activeElement instanceof HTMLElement
&& document.activeElement !== this.reference
) {
this.lastFocus = document.activeElement;
}
if (optionsRef.autofocus) {
this.reference.focus();
}
@@ -83,6 +92,7 @@ export function useFocus<T extends HTMLElement>({
}),
{
focus: observable.ref,
lastFocus: observable.ref,
reference: observable.ref,
setRef: action.bound,
updateFocus: action.bound,
@@ -124,5 +134,9 @@ export function useFocus<T extends HTMLElement>({
};
}, [state.reference]);
useEffect(() => () => {
state.lastFocus?.focus();
}, []);
return [state.setRef, state];
}
@@ -226,7 +226,7 @@ export function useMapResource<
try {
const active = await actions?.isActive?.(resource);
if (active === false || actions?.active === false) {
if (active === false) {
return;
}
@@ -374,6 +374,7 @@ export function useMapResource<
)
&& refObj.preloaded
&& keyRef.key !== null
&& actions?.active !== false
&& (
result.exception === null
|| (Array.isArray(result.exception) && !result.exception.some(Boolean))
@@ -386,6 +387,10 @@ export function useMapResource<
}
});
// if (canLoad && !refObj.loading) {
// throw (refObj as any)[loadFunctionName]();
// }
return result;
}
@@ -6,10 +6,11 @@
* you may not use this file except in compliance with the License.
*/
import { Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { Body } from '@cloudbeaver/core-app';
import { ErrorBoundary } from '@cloudbeaver/core-blocks';
import { ErrorBoundary, Loader } from '@cloudbeaver/core-blocks';
import { AppContext, IServiceInjector } from '@cloudbeaver/core-di';
export function renderLayout(serviceInjector: IServiceInjector): void {
@@ -19,7 +20,9 @@ export function renderLayout(serviceInjector: IServiceInjector): void {
root.render(
<ErrorBoundary root>
<AppContext app={serviceInjector}>
<Body />
<Suspense fallback={<Loader />}>
<Body />
</Suspense>
</AppContext>
</ErrorBoundary>
);
@@ -49,7 +49,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
.filter(([key, context]) => context.projectId === param.mark)
.map(([key]) => key)
),
true
(a, b) => a.mark === b.mark
);
permissionsResource
@@ -52,7 +52,7 @@ export class ConnectionFolderResource extends CachedMapResource<IConnectionFolde
this.addAlias(
isConnectionFolderProjectKey,
param => resourceKeyList(this.keys.filter(key => key.projectId === param.mark)),
true
(a, b) => a.mark === b.mark
);
makeObservable<this>(this, {
@@ -84,7 +84,7 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
this.addAlias(
isConnectionInfoProjectKey,
param => resourceKeyList(this.keys.filter(key => key.projectId === param.mark)),
true
(a, b) => a.mark === b.mark
);
// in case when session was refreshed all data depended on connection info
@@ -35,11 +35,11 @@ export const ExecutorInterrupter = {
};
},
interrupter<T>(flag: (data: T) => Promise<boolean> | boolean) {
interrupter<T>(flag: (data: T, context: IExecutionContextProvider<T>) => Promise<boolean> | boolean) {
return async (data: T, contexts: IExecutionContextProvider<T>): Promise<void> => {
const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext);
if ((await flag(data))) {
if ((await flag(data, contexts))) {
interrupt.interrupt();
}
};
@@ -33,7 +33,7 @@ export class DBObjectResource extends CachedMapResource<string, DBObject> {
this.addAlias(
isDBObjectParentKey,
param => resourceKeyList(navTreeResource.get(param.mark) || []),
true
(a, b) => a.mark === b.mark
);
// this.preloadResource(this.navNodeInfoResource);
this.navNodeInfoResource.outdateResource(this);
@@ -143,11 +143,14 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
return true;
}
async refreshTree(navNodeId: string): Promise<void> {
async refreshTree(navNodeId: string, silent = false): Promise<void> {
await this.graphQLService.sdk.navRefreshNode({
nodePath: navNodeId,
});
this.markTreeOutdated(navNodeId);
if (!silent) {
this.markTreeOutdated(navNodeId);
}
await this.onNodeRefresh.execute(navNodeId);
}
@@ -0,0 +1,14 @@
/*
* 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 enum EProjectPermission {
ConnectionRead = 'project-datasource-view',
ConnectionEdit = 'project-datasource-edit',
ResourceRead = 'project-resource-view',
ResourceEdit = 'project-resource-edit'
}
@@ -0,0 +1,30 @@
/*
* 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 { PermissionInfo } from '@cloudbeaver/core-administration';
import { injectable } from '@cloudbeaver/core-di';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { GraphQLService, CachedDataResource } from '@cloudbeaver/core-sdk';
@injectable()
export class ProjectPermissionsResource extends CachedDataResource<PermissionInfo[], void | any> {
constructor(
private readonly graphQLService: GraphQLService,
serverConfigResource: ServerConfigResource
) {
super([]);
this.sync(serverConfigResource, () => {}, () => {});
}
protected async loader(): Promise<PermissionInfo[]> {
const { permissions } = await this.graphQLService.sdk.getProjectPermissionsList();
return permissions;
}
}
@@ -8,14 +8,16 @@
import { runInAction } from 'mobx';
import { EAdminPermission } from '@cloudbeaver/core-administration';
import { AdminObjectGrantInfo, 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';
import { GraphQLService, CachedMapResource, CachedMapAllKey, ResourceKey, ResourceKeyUtils, resourceKeyList, RmProject, ResourceKeyList, RmProjectPermissions } from '@cloudbeaver/core-sdk';
import { isArraysEqual } from '@cloudbeaver/core-utils';
const newSymbol = Symbol('new-project');
export type SharedProject = RmProject;
export type ProjectPermission = RmProjectPermissions;
type SharedProjectNew = SharedProject & { [newSymbol]: boolean };
interface IProjectConfig {
@@ -44,6 +46,21 @@ export class SharedProjectsResource extends CachedMapResource<string, SharedProj
return newSymbol in this.get(id)!;
}
async setAccessSubjects(projectId: string, permissions: ProjectPermission[]): Promise<void> {
await this.graphQLService.sdk.setProjectPermissions({
projectId,
permissions,
});
}
async loadAccessSubjects(projectId: string): Promise<AdminObjectGrantInfo[]> {
const { grantedPermissions } = await this.graphQLService.sdk.getProjectGrantedPermissions({
projectId,
});
return grantedPermissions;
}
async create(config: IProjectConfig): Promise<SharedProject> {
const { project } = await this.graphQLService.sdk.createProject({
projectId: config.id,
@@ -74,7 +91,9 @@ export class SharedProjectsResource extends CachedMapResource<string, SharedProj
});
});
} finally {
this.delete(resourceKeyList(deleted));
if (deleted.length > 0) {
this.delete(resourceKeyList(deleted));
}
}
}
@@ -108,4 +127,13 @@ export class SharedProjectsResource extends CachedMapResource<string, SharedProj
const data = this.data.get(key);
this.data.set(key, Object.assign(data ?? {}, value));
}
}
export function isEqualSharedProjectGrantInfo(a: AdminObjectGrantInfo, b: AdminObjectGrantInfo): boolean {
return (
a.subjectId === b.subjectId
&& a.subjectType === b.subjectType
&& a.objectPermissions.objectId === b.objectPermissions.objectId
&& isArraysEqual(a.objectPermissions.permissions, b.objectPermissions.permissions)
);
}
@@ -1,2 +1,4 @@
export * from './SharedProjectsResource';
export * from './ProjectPermissionsResource';
export * from './EProjectPermission';
export { resourceManagerManifest } from './manifest';
@@ -8,6 +8,7 @@
import type { PluginManifest } from '@cloudbeaver/core-di';
import { ProjectPermissionsResource } from './ProjectPermissionsResource';
import { SharedProjectsResource } from './SharedProjectsResource';
@@ -18,5 +19,6 @@ export const resourceManagerManifest: PluginManifest = {
providers: [
SharedProjectsResource,
ProjectPermissionsResource,
],
};
@@ -25,12 +25,12 @@ export interface IDataError<TParam> {
}
export type IParamAlias<TParam> = {
getter: false;
param: TParam;
getAlias: (param: TParam) => TParam;
isEqual: undefined;
} | {
getter: true;
param: (param: TParam) => boolean;
isEqual: (paramA: TParam, paramB: TParam) => boolean;
getAlias: (param: TParam) => TParam;
};
@@ -227,7 +227,7 @@ export abstract class CachedResource<
isAlias(key: TParam): boolean {
return this.paramAliases.some(alias => {
if ('getter' in alias && alias.getter) {
if ('isEqual' in alias && alias.isEqual) {
return alias.param(key);
} else {
return alias.param === key;
@@ -344,11 +344,19 @@ export abstract class CachedResource<
addAlias<T extends TParam>(
param: (param: TParam) => param is T,
getAlias: (param: T) => TParam,
getter: true
isEqual: (paramA: T, paramB: T) => boolean
): void;
addAlias(param: (param: TParam) => boolean, getAlias: (param: TParam) => TParam, getter: true): void;
addAlias(param: TParam | ((param: TParam) => boolean), getAlias: (param: TParam) => TParam, getter?: boolean): void {
this.paramAliases.push({ param, getAlias, getter } as IParamAlias<TParam>);
addAlias(
param: (param: TParam) => boolean,
getAlias: (param: TParam) => TParam,
isEqual: (paramA: TParam, paramB: TParam) => boolean
): void;
addAlias(
param: TParam | ((param: TParam) => boolean),
getAlias: (param: TParam) => TParam,
isEqual?: (paramA: TParam, paramB: TParam) => boolean
): void {
this.paramAliases.push({ param, getAlias, isEqual } as IParamAlias<TParam>);
}
transformParam(param: TParam): TParam {
@@ -358,7 +366,7 @@ export abstract class CachedResource<
if (deep < 10) {
for (const alias of this.paramAliases) {
if ('getter' in alias && alias.getter) {
if ('isEqual' in alias && alias.isEqual) {
if (alias.param(param)) {
param = alias.getAlias(param);
deep++;
@@ -452,8 +460,8 @@ export abstract class CachedResource<
}
return this.paramAliases.some(alias => {
if ('getter' in alias && alias.getter) {
return alias.param(param) && alias.param(second);
if ('isEqual' in alias && alias.isEqual) {
return alias.param(param) && alias.param(second) && alias.isEqual(param, second);
}
return false;
@@ -0,0 +1,8 @@
fragment AdminObjectGrantInfo on AdminObjectGrantInfo {
subjectId
subjectType
objectPermissions {
objectId
permissions
}
}
@@ -3,7 +3,9 @@ query getProjectList {
id
name
description
canCreateConnections
canViewConnections
canEditDataSources
canViewDataSources
canEditResources
canViewResources
}
}
@@ -0,0 +1,9 @@
query getProjectGrantedPermissions(
$projectId: String!
) {
grantedPermissions: rmListProjectGrantedPermissions(
projectId: $projectId
) {
...AdminObjectGrantInfo
}
}
@@ -0,0 +1,5 @@
query getProjectPermissionsList {
permissions: rmListProjectPermissions {
...AdminPermissionInfo
}
}
@@ -0,0 +1,9 @@
mutation setProjectPermissions(
$projectId: String!
$permissions: [RMProjectPermissions!]!
) {
rmSetProjectPermissions(
projectId: $projectId
permissions: $permissions
)
}
+105 -5
View File
@@ -47,6 +47,17 @@ export interface AdminConnectionSearchInfo {
possibleDrivers: Array<Scalars['ID']>;
}
export interface AdminObjectGrantInfo {
objectPermissions: AdminObjectPermissions;
subjectId: Scalars['ID'];
subjectType: AdminSubjectType;
}
export interface AdminObjectPermissions {
objectId: Scalars['ID'];
permissions: Array<Scalars['String']>;
}
export interface AdminPermissionInfo {
category?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
@@ -389,6 +400,7 @@ export interface Mutation {
rmDeleteProject: Scalars['Boolean'];
rmDeleteResource?: Maybe<Scalars['Boolean']>;
rmMoveResource: Scalars['String'];
rmSetProjectPermissions: Scalars['Boolean'];
rmWriteResourceStringContent: Scalars['String'];
setConnectionNavigatorSettings: ConnectionInfo;
setUserConfigurationParameter: Scalars['Boolean'];
@@ -581,6 +593,12 @@ export interface MutationRmMoveResourceArgs {
}
export interface MutationRmSetProjectPermissionsArgs {
permissions: Array<RmProjectPermissions>;
projectId: Scalars['String'];
}
export interface MutationRmWriteResourceStringContentArgs {
data: Scalars['String'];
forceOverwrite: Scalars['Boolean'];
@@ -825,8 +843,10 @@ export interface ProductInfo {
}
export interface ProjectInfo {
canCreateConnections: Scalars['Boolean'];
canViewConnections: Scalars['Boolean'];
canEditDataSources: Scalars['Boolean'];
canEditResources: Scalars['Boolean'];
canViewDataSources: Scalars['Boolean'];
canViewResources: Scalars['Boolean'];
description?: Maybe<Scalars['String']>;
id: Scalars['String'];
name: Scalars['String'];
@@ -880,9 +900,12 @@ export interface Query {
networkHandlers: Array<NetworkHandlerDescriptor>;
readSessionLog: Array<LogEntry>;
revokeUserRole?: Maybe<Scalars['Boolean']>;
rmListProjectGrantedPermissions: Array<AdminObjectGrantInfo>;
rmListProjectPermissions: Array<AdminPermissionInfo>;
rmListProjects: Array<RmProject>;
rmListResources: Array<RmResource>;
rmListSharedProjects: Array<RmProject>;
rmListSubjectProjectsPermissionGrants: Array<AdminObjectGrantInfo>;
rmProject: RmProject;
rmReadResourceAsString: Scalars['String'];
saveAuthProviderConfiguration: AdminAuthProviderConfiguration;
@@ -1129,6 +1152,11 @@ export interface QueryRevokeUserRoleArgs {
}
export interface QueryRmListProjectGrantedPermissionsArgs {
projectId: Scalars['String'];
}
export interface QueryRmListResourcesArgs {
folder?: InputMaybe<Scalars['String']>;
nameMask?: InputMaybe<Scalars['String']>;
@@ -1138,6 +1166,11 @@ export interface QueryRmListResourcesArgs {
}
export interface QueryRmListSubjectProjectsPermissionGrantsArgs {
subjectId: Scalars['String'];
}
export interface QueryRmProjectArgs {
projectId: Scalars['String'];
}
@@ -1311,6 +1344,11 @@ export interface RmProject {
shared: Scalars['Boolean'];
}
export interface RmProjectPermissions {
permissions: Array<Scalars['String']>;
subjectId: Scalars['String'];
}
export interface RmResource {
folder: Scalars['Boolean'];
length: Scalars['Int'];
@@ -2189,6 +2227,8 @@ export type NavGetStructContainersQueryVariables = Exact<{
export type NavGetStructContainersQuery = { navGetStructContainers: { supportsCatalogChange: boolean, supportsSchemaChange: boolean, catalogList: Array<{ catalog: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> }>, schemaList: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> } };
export type AdminObjectGrantInfoFragment = { subjectId: string, subjectType: AdminSubjectType, objectPermissions: { objectId: string, permissions: Array<string> } };
export type AdminPermissionInfoFragment = { id: string, label?: string, description?: string, category?: string };
export type AdminRoleInfoFragment = { roleId: string, roleName?: string, description?: string, rolePermissions: Array<string> };
@@ -2427,7 +2467,7 @@ export type NavRenameNodeMutation = { navRenameNode?: string };
export type GetProjectListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canCreateConnections: boolean, canViewConnections: boolean }> };
export type GetProjectListQuery = { projects: Array<{ id: string, name: string, description?: string, canEditDataSources: boolean, canViewDataSources: boolean, canEditResources: boolean, canViewResources: boolean }> };
export type CreateProjectMutationVariables = Exact<{
projectId?: InputMaybe<Scalars['ID']>;
@@ -2470,6 +2510,18 @@ export type GetProjectQueryVariables = Exact<{
export type GetProjectQuery = { project: { id: string, name: string, shared: boolean, description?: string, projectPermissions: Array<string> } };
export type GetProjectGrantedPermissionsQueryVariables = Exact<{
projectId: Scalars['String'];
}>;
export type GetProjectGrantedPermissionsQuery = { grantedPermissions: Array<{ subjectId: string, subjectType: AdminSubjectType, objectPermissions: { objectId: string, permissions: Array<string> } }> };
export type GetProjectPermissionsListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetProjectPermissionsListQuery = { permissions: Array<{ id: string, label?: string, description?: string, category?: string }> };
export type GetResourceListQueryVariables = Exact<{
projectId: Scalars['String'];
folder?: InputMaybe<Scalars['String']>;
@@ -2508,6 +2560,14 @@ export type ReadResourceQueryVariables = Exact<{
export type ReadResourceQuery = { value: string };
export type SetProjectPermissionsMutationVariables = Exact<{
projectId: Scalars['String'];
permissions: Array<RmProjectPermissions> | RmProjectPermissions;
}>;
export type SetProjectPermissionsMutation = { rmSetProjectPermissions: boolean };
export type WriteResourceContentMutationVariables = Exact<{
projectId: Scalars['String'];
resourcePath: Scalars['String'];
@@ -2640,6 +2700,16 @@ export type SqlGenerateEntityQueryQueryVariables = Exact<{
export type SqlGenerateEntityQueryQuery = { sqlGenerateEntityQuery: string };
export const AdminObjectGrantInfoFragmentDoc = `
fragment AdminObjectGrantInfo on AdminObjectGrantInfo {
subjectId
subjectType
objectPermissions {
objectId
permissions
}
}
`;
export const AdminPermissionInfoFragmentDoc = `
fragment AdminPermissionInfo on AdminPermissionInfo {
id
@@ -3928,8 +3998,10 @@ export const GetProjectListDocument = `
id
name
description
canCreateConnections
canViewConnections
canEditDataSources
canViewDataSources
canEditResources
canViewResources
}
}
`;
@@ -3974,6 +4046,20 @@ export const GetProjectDocument = `
}
}
${SharedProjectFragmentDoc}`;
export const GetProjectGrantedPermissionsDocument = `
query getProjectGrantedPermissions($projectId: String!) {
grantedPermissions: rmListProjectGrantedPermissions(projectId: $projectId) {
...AdminObjectGrantInfo
}
}
${AdminObjectGrantInfoFragmentDoc}`;
export const GetProjectPermissionsListDocument = `
query getProjectPermissionsList {
permissions: rmListProjectPermissions {
...AdminPermissionInfo
}
}
${AdminPermissionInfoFragmentDoc}`;
export const GetResourceListDocument = `
query getResourceList($projectId: String!, $folder: String, $nameMask: String, $readProperties: Boolean, $readHistory: Boolean) {
resources: rmListResources(
@@ -4024,6 +4110,11 @@ export const ReadResourceDocument = `
)
}
`;
export const SetProjectPermissionsDocument = `
mutation setProjectPermissions($projectId: String!, $permissions: [RMProjectPermissions!]!) {
rmSetProjectPermissions(projectId: $projectId, permissions: $permissions)
}
`;
export const WriteResourceContentDocument = `
mutation writeResourceContent($projectId: String!, $resourcePath: String!, $data: String!, $forceOverwrite: Boolean!) {
rmWriteResourceStringContent(
@@ -4535,6 +4626,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getProject(variables: GetProjectQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectQuery>(GetProjectDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProject', 'query');
},
getProjectGrantedPermissions(variables: GetProjectGrantedPermissionsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectGrantedPermissionsQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectGrantedPermissionsQuery>(GetProjectGrantedPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectGrantedPermissions', 'query');
},
getProjectPermissionsList(variables?: GetProjectPermissionsListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetProjectPermissionsListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetProjectPermissionsListQuery>(GetProjectPermissionsListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getProjectPermissionsList', 'query');
},
getResourceList(variables: GetResourceListQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetResourceListQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetResourceListQuery>(GetResourceListDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getResourceList', 'query');
},
@@ -4550,6 +4647,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
readResource(variables: ReadResourceQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<ReadResourceQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<ReadResourceQuery>(ReadResourceDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'readResource', 'query');
},
setProjectPermissions(variables: SetProjectPermissionsMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<SetProjectPermissionsMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<SetProjectPermissionsMutation>(SetProjectPermissionsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'setProjectPermissions', 'mutation');
},
writeResourceContent(variables: WriteResourceContentMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<WriteResourceContentMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<WriteResourceContentMutation>(WriteResourceContentDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'writeResourceContent', 'mutation');
},
@@ -28,4 +28,8 @@ describe('Is array equals', () => {
test('should return "false" when arrays length is different and the second array is larger', () => {
expect(isArraysEqual([1, true], [1, true, '1'])).toBe(false);
});
test('should use isEqual argument if passed', () => {
expect(isArraysEqual([{ a:1 }], [{ a: 1 }], (a, b) => a.a === b.a)).toBe(true);
});
});
@@ -6,10 +6,16 @@
* you may not use this file except in compliance with the License.
*/
export function isArraysEqual<T>(first: T[], second: T[]): boolean {
const isObjectEqual = <T>(a: T, b: T) => a === b;
export function isArraysEqual<T>(
first: T[],
second: T[],
isEqual: (a: T, b: T) => boolean = isObjectEqual
): boolean {
if (first.length !== second.length) {
return false;
}
return !first.some(value => !second.includes(value));
return !first.some(b => !second.some(a => isEqual(a, b)));
}
@@ -36,7 +36,7 @@ export class CustomConnectionPluginBootstrap extends Bootstrap {
title: 'app_shared_connectionMenu_custom',
isHidden: () => (
!this.serverConfigResource.data?.supportsCustomConnections
|| !this.projectsService.activeProject?.canCreateConnections
|| !this.projectsService.activeProject?.canEditDataSources
),
onClick: () => this.openConnectionsDialog(),
});
@@ -36,7 +36,7 @@ export class SearchConnectionPluginBootstrap extends Bootstrap {
order: 3,
title: 'connections_connection_create_search_database',
isHidden: () => (
!this.projectsService.activeProject?.canCreateConnections
!this.projectsService.activeProject?.canEditDataSources
|| !this.permissionsService.has(EAdminPermission.admin)
),
onClick: () => {
@@ -136,7 +136,7 @@ export class ConnectionOptionsTabService extends Bootstrap {
if (state.projectId !== null && state.mode === 'create') {
const project = this.projectsResource.get(state.projectId);
if (!project?.canCreateConnections) {
if (!project?.canEditDataSources) {
validation.error('plugin_connections_connection_form_project_invalid');
}
}
@@ -40,11 +40,11 @@ import { CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cl
import { useStyles } from '@cloudbeaver/core-theming';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
import { ConnectionFormService } from '../ConnectionFormService';
import type { IConnectionFormProps } from '../IConnectionFormProps';
import { ParametersForm } from './ParametersForm';
import { ProjectSelect } from './ProjectSelect';
import { useOptions } from './useOptions';
const styles = css`
@@ -12,16 +12,17 @@ import { untracked } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { ConnectionFolder, ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
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 { ENodeMoveType, getNodesFromContext, INodeMoveData, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, nodeDeleteContext, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
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';
import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, type IElementsTree } from '@cloudbeaver/plugin-navigation-tree';
import { FolderDialog } from '@cloudbeaver/plugin-projects';
import { NAV_NODE_TYPE_CONNECTION } from './NAV_NODE_TYPE_CONNECTION';
@@ -30,6 +31,7 @@ interface ITargetNode {
folderId?: string;
projectNodeId: string;
selectProject: boolean;
}
@injectable()
@@ -47,7 +49,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
private readonly commonDialogService: CommonDialogService,
private readonly notificationService: NotificationService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly projectsResource: ProjectInfoResource,
private readonly projectInfoResource: ProjectInfoResource,
private readonly projectsNavNodeService: ProjectsNavNodeService
) {
super();
@@ -106,7 +108,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
const tree = context.tryGet(DATA_CONTEXT_ELEMENTS_TREE);
if (!tree || !this.userInfoResource.data) {
if (!tree || !this.userInfoResource.data || tree.baseRoot !== ROOT_NODE_PATH) {
return false;
}
const targetNode = this.getTargetNode(tree);
@@ -159,7 +161,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
return;
}
await this.projectsResource.load(CachedMapAllKey);
await this.projectInfoResource.load(CachedMapAllKey);
const move = contexts.getContext(navNodeMoveContext);
const nodes = getNodesFromContext(moveContexts);
@@ -185,7 +187,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
}
if (type === ENodeMoveType.CanDrop) {
if (targetProject?.canCreateConnections) {
if (targetProject?.canEditDataSources) {
move.setCanMove(true);
}
} else {
@@ -253,34 +255,38 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
await this.connectionFolderResource.load(ConnectionFolderProjectKey(targetNode.projectId));
let folder: ConnectionFolder | undefined;
let parentFolder: ConnectionFolder | undefined;
if (targetNode.folderId) {
folder = this.connectionFolderResource.fromNodeId(targetNode.folderId);
parentFolder = this.connectionFolderResource.fromNodeId(targetNode.folderId);
if (!folder) {
if (!parentFolder) {
this.notificationService.logError({ title:'Can\'t create folder', message: 'Folder not found' });
return;
}
}
const result = await this.commonDialogService.open(RenameDialog, {
const result = await this.commonDialogService.open(FolderDialog, {
value: this.localizationService.translate('ui_folder_new'),
projectId: targetNode.projectId,
title: 'core_view_action_new_folder',
subTitle: folder?.id,
subTitle: parentFolder?.id,
icon: '/icons/folder.svg#root',
create: true,
validation: (name, setMessage) => {
const trimmed = name.trim();
selectProject: targetNode.selectProject,
validation: async ({ folder, projectId }, setMessage) => {
const trimmed = folder.trim();
if (trimmed.length === 0 || !name.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
setMessage('connections_connection_folder_validation');
return false;
}
await this.connectionFolderResource.load(ConnectionFolderProjectKey(projectId));
return !this.connectionFolderResource.has(createConnectionFolderParam(
targetNode.projectId,
createPath(folder?.id, trimmed)
projectId,
createPath(parentFolder?.id, trimmed)
));
},
});
@@ -288,13 +294,13 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
try {
await this.connectionFolderResource.create(createConnectionFolderParam(
targetNode.projectId,
result
), folder?.id);
result.projectId,
result.folder
), parentFolder?.id);
this.navTreeResource.markOutdated(
targetNode.folderId !== undefined
? targetNode.folderId
: targetNode.projectNodeId
: this.projectsNavNodeService.getProjectNodeId(result.projectId)
);
} catch (exception: any) {
this.notificationService.logException(exception, 'Can\'t create folder');
@@ -318,10 +324,22 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
}
private getTargetNode(tree: IElementsTree): ITargetNode | undefined {
untracked(() => this.projectInfoResource.load(CachedMapAllKey));
const selected = tree.getSelected();
if (selected.length === 0) {
selected.push(tree.root);
const editableProjects = this.projectInfoResource.values.filter(project => project.canEditDataSources);
if (editableProjects.length > 0) {
const project = editableProjects[0];
return {
projectId: project.id,
projectNodeId: this.projectsNavNodeService.getProjectNodeId(project.id),
selectProject: editableProjects.length > 1,
};
}
return;
}
const targetFolder = selected[0];
@@ -333,11 +351,10 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
return;
}
untracked(() => this.projectsResource.load(CachedMapAllKey));
const project = this.projectsNavNodeService.getByNodeId(projectNode.id);
if (!project?.canCreateConnections) {
if (!project?.canEditDataSources) {
return;
}
@@ -350,6 +367,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
projectId: project.id,
folderId: targetFolderNode?.id,
projectNodeId: projectNode.id,
selectProject: false,
};
}
}
@@ -2,7 +2,6 @@ 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';
@@ -14,7 +14,6 @@ import { DBDriverResource, ConnectionInfoResource } from '@cloudbeaver/core-conn
import { useStyles } from '@cloudbeaver/core-theming';
import type { MenuBaseItemIconComponent } from '@cloudbeaver/core-view';
import { ConnectionSelector } from './ConnectionSelector';
import type { IConnectionSelectorExtraProps } from './IConnectionSelectorExtraProps';
const connectionIconStyle = css`
@@ -32,13 +31,13 @@ export const ConnectionIcon: MenuBaseItemIconComponent<IConnectionSelectorExtraP
const styles = useStyles(style, connectionIconStyle);
const connection = useMapResource(
ConnectionSelector,
ConnectionInfo,
ConnectionInfoResource,
connectionKey
);
const driverId = connection.data?.driverId;
const driver = useMapResource(ConnectionSelector, DBDriverResource, driverId!, {
const driver = useMapResource(ConnectionInfo, DBDriverResource, driverId!, {
active: driverId !== undefined,
});
@@ -94,7 +94,7 @@ export const NavigationTabsBar = observer<Props>(function NavigationTabsBar({ cl
>
{navigation.tabIdList.map(tabId => (
<TabPanel key={tabId} tabId={tabId} lazy>
<TabHandlerPanel tabId={tabId} />
{() => <TabHandlerPanel tabId={tabId} />}
</TabPanel>
))}
</TabsBox>
@@ -15,6 +15,7 @@ import { NotificationService } from '@cloudbeaver/core-events';
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { ACTION_OPEN_IN_TAB, IActiveView, View } from '@cloudbeaver/core-view';
import type { ITab } from './ITab';
@@ -90,7 +91,9 @@ export class NavigationTabsService extends View<ITab> {
state: observable,
currentTab: computed,
currentTabId: computed,
tabIdList: computed,
tabIdList: computed<string[]>({
equals: isArraysEqual,
}),
openTab: action,
selectTab: action,
closeTab: action,
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { useMemo, useCallback, useEffect } from 'react';
import styled, { css, use } from 'reshadow';
import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag, FolderExplorer, FolderExplorerPath, Loader, TreeNodeNested, TreeNodeNestedMessage, TREE_NODE_STYLES, useFolderExplorer, useMapResource, useObjectRef } from '@cloudbeaver/core-blocks';
import { EventTreeNodeClickFlag, EventTreeNodeExpandFlag, EventTreeNodeSelectFlag, FolderExplorer, FolderExplorerPath, Loader, PlaceholderElement, TreeNodeNested, TreeNodeNestedMessage, TREE_NODE_STYLES, useFolderExplorer, useMapResource, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { Translate } from '@cloudbeaver/core-localization';
@@ -22,6 +22,7 @@ import { IElementsTreeContext, ElementsTreeContext } from './ElementsTreeContext
import { ElementsTreeLoader } from './ElementsTreeLoader';
import { elementsTreeNameFilter } from './elementsTreeNameFilter';
import { ElementsTreeTools } from './ElementsTreeTools/ElementsTreeTools';
import type { IElementsTreeSettingsProps } from './ElementsTreeTools/NavigationTreeSettings/ElementsTreeSettingsService';
import type { NavTreeControlComponent } from './NavigationNodeComponent';
import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested';
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
@@ -44,6 +45,14 @@ const styles = css`
display: flex;
flex: 1;
flex-direction: column;
& tree-elements {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
}
}
tree-box {
@@ -86,6 +95,7 @@ interface Props extends IElementsTreeOptions {
big?: boolean;
style?: ComponentStyle;
className?: string;
settingsElements?: PlaceholderElement<IElementsTreeSettingsProps>[];
navNodeFilterCompare?: NavNodeFilterCompareFn;
onClick?: (node: NavNode) => Promise<void> | void;
onOpen?: (node: NavNode, folder: boolean) => Promise<void> | void;
@@ -104,6 +114,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
filters = [],
renderers = [],
expandStateGetters,
settingsElements,
big,
style,
className,
@@ -289,45 +300,47 @@ export const ElementsTree = observer<Props>(function ElementsTree({
return styled(useStyles(TREE_NODE_STYLES, styles, style))(
<>
<ElementsTreeTools tree={tree} style={style} />
<ElementsTreeTools tree={tree} settingsElements={settingsElements} style={style} />
<tree-box {...use({ big })}>
<ElementsTreeLoader
root={root}
context={context}
emptyPlaceholder={emptyPlaceholder}
childrenState={tree}
hasChildren={hasChildren}
>
<ElementsTreeContext.Provider value={context}>
<box className={className}>
<FolderExplorer state={folderExplorer}>
<tree ref={dropOutside.mouse.reference} as="div" onClick={handleClick}>
{settings?.showFolderExplorerPath && <FolderExplorerPath getName={getName} canSkip={canSkip} />}
<drop-outside
ref={dndBox.setRef}
{...use({
showDropOutside: dropOutside.showDropOutside,
active: dropOutside.zoneActive,
bottom: dropOutside.bottom,
})}
>
<TreeNodeNested root>
<TreeNodeNestedMessage><Translate token='app_navigationTree_drop_here' /></TreeNodeNestedMessage>
</TreeNodeNested>
</drop-outside>
<NavigationNodeNested
ref={dropOutside.nestedRef}
nodeId={root}
component={NavigationNodeElement}
path={folderExplorer.state.path}
root
/>
<ElementsTreeContext.Provider value={context}>
<box className={className}>
<FolderExplorer state={folderExplorer}>
<tree ref={dropOutside.mouse.reference} as="div" onClick={handleClick}>
{settings?.showFolderExplorerPath && <FolderExplorerPath getName={getName} canSkip={canSkip} />}
<drop-outside
ref={dndBox.setRef}
{...use({
showDropOutside: dropOutside.showDropOutside,
active: dropOutside.zoneActive,
bottom: dropOutside.bottom,
})}
>
<TreeNodeNested root>
<TreeNodeNestedMessage><Translate token='app_navigationTree_drop_here' /></TreeNodeNestedMessage>
</TreeNodeNested>
</drop-outside>
<ElementsTreeLoader
root={root}
context={context}
emptyPlaceholder={emptyPlaceholder}
childrenState={tree}
hasChildren={hasChildren}
>
<tree-elements>
<NavigationNodeNested
ref={dropOutside.nestedRef}
nodeId={root}
component={NavigationNodeElement}
path={folderExplorer.state.path}
root
/>
</tree-elements>
{loaderAvailable && <Loader state={tree} overlay={hasChildren} />}
</tree>
</FolderExplorer>
</box>
</ElementsTreeContext.Provider>
</ElementsTreeLoader>
</ElementsTreeLoader>
</tree>
</FolderExplorer>
</box>
</ElementsTreeContext.Provider>
</tree-box>
</>
);
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import React, { useState } from 'react';
import styled, { css, use } from 'reshadow';
import { IconButton, useMapResource } from '@cloudbeaver/core-blocks';
import { IconButton, PlaceholderElement, useMapResource } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
@@ -21,6 +21,7 @@ import type { IElementsTree } from '../useElementsTree';
import { ElementsTreeFilter } from './ElementsTreeFilter';
import { ElementsTreeToolsMenu } from './ElementsTreeToolsMenu';
import { DATA_CONTEXT_NAV_TREE_ROOT } from './NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT';
import type { IElementsTreeSettingsProps } from './NavigationTreeSettings/ElementsTreeSettingsService';
import { NavigationTreeSettings } from './NavigationTreeSettings/NavigationTreeSettings';
const toolsStyles = css`
@@ -73,11 +74,13 @@ const toolsStyles = css`
interface Props {
tree: IElementsTree;
settingsElements?: PlaceholderElement<IElementsTreeSettingsProps>[];
style?: ComponentStyle;
}
export const ElementsTreeTools = observer<React.PropsWithChildren<Props>>(function ElementsTreeTools({
tree,
settingsElements,
style,
children,
}) {
@@ -120,7 +123,7 @@ export const ElementsTreeTools = observer<React.PropsWithChildren<Props>>(functi
/>
</actions>
{tree.settings && opened && (
<NavigationTreeSettings root={tree.baseRoot} settings={tree.settings} style={style} />
<NavigationTreeSettings tree={tree} elements={settingsElements} style={style} />
)}
<ElementsTreeFilter tree={tree} style={style} />
{children}
@@ -0,0 +1,78 @@
/*
* 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 from 'reshadow';
import { type PlaceholderComponent, BASE_CONTAINERS_STYLES, GroupTitle, Switch } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import type { IElementsTreeSettingsProps } from './ElementsTreeSettingsService';
export const ElementsTreeBaseSettingsForm: PlaceholderComponent<IElementsTreeSettingsProps> = observer(function ElementsTreeBaseSettingsForm({
tree: { root, settings },
style,
}) {
const styles = useStyles(BASE_CONTAINERS_STYLES, style);
const translate = useTranslate();
if (!settings) {
return null;
}
return styled(styles)(
<>
<GroupTitle>{translate('ui_settings')}</GroupTitle>
<Switch
id={`${root}.filter`}
name="filter"
state={settings}
disabled={!settings.configurable}
title={translate('app_navigationTree_settings_filter_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_filter_title')}
</Switch>
<Switch
id={`${root}.filterAll`}
name="filterAll"
state={settings}
disabled={!settings.filter || !settings.configurable}
title={translate('app_navigationTree_settings_filter_all_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_filter_all_title')}
</Switch>
<Switch
id={`${root}.saveExpanded`}
name="saveExpanded"
state={settings}
disabled={!settings.configurable}
title={translate('app_navigationTree_settings_state_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_state_title')}
</Switch>
<Switch
id={`${root}.foldersTree`}
name="foldersTree"
state={settings}
disabled={!settings.configurable}
title={translate('app_navigationTree_settings_folders_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_folders_title')}
</Switch>
</>
);
});
@@ -0,0 +1,28 @@
/*
* 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 { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { injectable } from '@cloudbeaver/core-di';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import type { IElementsTree } from '../../useElementsTree';
import { ElementsTreeBaseSettingsForm } from './ElementsTreeBaseSettingsForm';
export interface IElementsTreeSettingsProps {
tree: IElementsTree;
style?: ComponentStyle;
}
@injectable()
export class ElementsTreeSettingsService {
readonly placeholder = new PlaceholderContainer<IElementsTreeSettingsProps>();
constructor() {
this.placeholder.add(ElementsTreeBaseSettingsForm, 0);
}
}
@@ -8,7 +8,6 @@
import { createKeyBinding } from '@cloudbeaver/core-view';
export const KEY_BINDING_ENABLE_FILTER = createKeyBinding({
id: 'enable-filter',
label: 'Ctrl + F',
@@ -9,11 +9,11 @@
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { BASE_CONTAINERS_STYLES, Group, GroupTitle, Switch } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { BASE_CONTAINERS_STYLES, Group, Placeholder, PlaceholderElement } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import type { IElementsTreeSettings } from '../../useElementsTree';
import { ElementsTreeSettingsService, IElementsTreeSettingsProps } from './ElementsTreeSettingsService';
const expandStyles = css`
settings {
@@ -30,70 +30,29 @@ const expandStyles = css`
}
`;
interface Props {
root: string;
settings: IElementsTreeSettings;
style?: ComponentStyle;
interface Props extends IElementsTreeSettingsProps {
elements?: PlaceholderElement<IElementsTreeSettingsProps>[];
className?: string;
}
export const NavigationTreeSettings = observer<Props>(function NavigationTreeSettings({
root,
settings,
elements,
tree,
style,
className,
}) {
const styles = useStyles(BASE_CONTAINERS_STYLES, expandStyles, style);
const translate = useTranslate();
const elementsTreeSettingsService = useService(ElementsTreeSettingsService);
return styled(styles)(
<settings className={className}>
<Group keepSize form gap dense>
<GroupTitle>{translate('ui_settings')}</GroupTitle>
<Switch
id={`${root}.filter`}
name="filter"
state={settings}
disabled={!settings.configurable}
title={translate('app_navigationTree_settings_filter_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_filter_title')}
</Switch>
<Switch
id={`${root}.filterAll`}
name="filterAll"
state={settings}
disabled={!settings.filter || !settings.configurable}
title={translate('app_navigationTree_settings_filter_all_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_filter_all_title')}
</Switch>
<Switch
id={`${root}.saveExpanded`}
name="saveExpanded"
state={settings}
disabled={!settings.configurable}
title={translate('app_navigationTree_settings_state_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_state_title')}
</Switch>
<Switch
id={`${root}.foldersTree`}
name="foldersTree"
state={settings}
disabled={!settings.configurable}
title={translate('app_navigationTree_settings_folders_description')}
mod={['primary', 'dense']}
small
>
{translate('app_navigationTree_settings_folders_title')}
</Switch>
<Placeholder
container={elementsTreeSettingsService.placeholder}
elements={elements}
tree={tree}
style={style}
/>
</Group>
</settings>
);
@@ -17,6 +17,7 @@ export function createElementsTreeSettings(defaults?: Partial<IElementsTreeSetti
foldersTree: false,
showFolderExplorerPath: true,
configurable: true,
projects: true,
...defaults,
};
}
@@ -31,5 +32,6 @@ export function validateElementsTreeSettings(data: any): boolean {
&& typeof data.foldersTree === 'boolean'
&& typeof data.showFolderExplorerPath === 'boolean'
&& typeof data.configurable === 'boolean'
&& typeof data.projects === 'boolean'
);
}
@@ -20,7 +20,7 @@ function isDefined<T>(val: T | undefined | null): val is T {
export function elementsTreeNameFilter(
navTreeResource: NavTreeResource,
navNodeInfoResource: NavNodeInfoResource,
compare: NavNodeFilterCompareFn = compareNodes
compare: NavNodeFilterCompareFn = elementsTreeNameFilterNode
): IElementsTreeFilter {
return (filter: string, node: NavNode, children: string[], state: MetadataMap<string, ITreeNodeState>) => {
const nodeState = state.get(node.id);
@@ -79,7 +79,7 @@ function filterNode(
// return false;
}
function compareNodes(node: NavNode, filter: string): EEquality {
export function elementsTreeNameFilterNode(node: NavNode, filter: string): EEquality {
const nodeName = node.name?.toLowerCase().trim();
const filterToLower = filter.toLowerCase().trim();
@@ -59,6 +59,7 @@ export interface IElementsTreeSettings {
saveFilter: boolean;
showFolderExplorerPath: boolean;
configurable: boolean;
projects: boolean;
}
export interface IElementsTreeOptions {
@@ -27,8 +27,10 @@ import { getNavigationTreeUserSettingsId } from './getNavigationTreeUserSettings
import { navigationTreeDuplicateFilter } from './navigationTreeDuplicateIdFilter';
import { NavigationTreeService } from './NavigationTreeService';
import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter';
import { navigationTreeProjectSearchCompare } from './ProjectsRenderer/navigationTreeProjectSearchCompare';
import { navigationTreeProjectsExpandStateGetter } from './ProjectsRenderer/navigationTreeProjectsExpandStateGetter';
import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navigationTreeProjectsRendererRenderer';
import { ProjectsSettingsForm, ProjectsSettingsPlaceholderElement } from './ProjectsRenderer/ProjectsSettingsForm';
import { useNavigationTree } from './useNavigationTree';
const navigationTreeStyles = css`
@@ -108,6 +110,8 @@ export const NavigationTree = observer(function NavigationTree() {
[navNodeInfoResource, navTreeResource]
);
const settingsElements = useMemo(() => ([ProjectsSettingsPlaceholderElement]), []);
if (!isEnabled) {
return null;
}
@@ -119,7 +123,9 @@ export const NavigationTree = observer(function NavigationTree() {
localState={navTreeService.treeState}
filters={[duplicateFilter, connectionGroupFilter, projectFilter]}
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer]}
navNodeFilterCompare={navigationTreeProjectSearchCompare}
expandStateGetters={[projectsExpandStateGetter]}
settingsElements={settingsElements}
emptyPlaceholder={() => styled(navigationTreeStyles)(
<center>
<message>
@@ -15,6 +15,7 @@ import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree';
import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext';
import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent';
import { NavigationNodeEditor } from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor';
import { TreeNodeMenu } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu';
@@ -62,6 +63,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
className,
}, ref) {
const treeNodeContext = useContext(TreeNodeContext);
const elementsTreeContext = useContext(ElementsTreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
const selected = treeNodeContext.selected;
@@ -83,6 +85,10 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
if (elementsTreeContext?.tree.settings?.projects === false) {
return null;
}
return styled(TREE_NODE_STYLES, styles)(
<TreeNodeControl
ref={ref}
@@ -0,0 +1,51 @@
/*
* 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 from 'reshadow';
import { type PlaceholderComponent, BASE_CONTAINERS_STYLES, Switch, PlaceholderElement } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import type { IElementsTreeSettingsProps } from '../ElementsTree/ElementsTreeTools/NavigationTreeSettings/ElementsTreeSettingsService';
export const ProjectsSettingsForm: PlaceholderComponent<IElementsTreeSettingsProps> = observer(function ProjectsSettingsForm({
tree: { root, settings },
style,
}) {
const styles = useStyles(BASE_CONTAINERS_STYLES, style);
const translate = useTranslate();
if (!settings) {
return null;
}
return styled(styles)(
<>
<Switch
id={`${root}.projects`}
name="projects"
state={settings}
disabled={!settings.configurable}
title={translate('plugin_navigation_tree_settings_projects_description')}
mod={['primary', 'dense']}
small
>
{translate('plugin_navigation_tree_settings_projects_title')}
</Switch>
</>
);
});
export const ProjectsSettingsPlaceholderElement: PlaceholderElement<IElementsTreeSettingsProps> = {
id: 'settings-project',
component: ProjectsSettingsForm,
order: 1,
};
@@ -0,0 +1,21 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
import { EEquality, elementsTreeNameFilterNode, NavNodeFilterCompareFn } from '@cloudbeaver/plugin-navigation-tree';
export const navigationTreeProjectSearchCompare: NavNodeFilterCompareFn = function navigationTreeProjectSearchCompare(
node,
filter
) {
if (node.nodeType === NAV_NODE_TYPE_PROJECT) {
return EEquality.none;
}
return elementsTreeNameFilterNode(node, filter);
};
@@ -7,7 +7,8 @@
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { useContext } from 'react';
import styled, { css, use } from 'reshadow';
import { TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { Translate } from '@cloudbeaver/core-localization';
@@ -15,6 +16,7 @@ import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
import { useNode } from '../../NodesManager/useNode';
import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext';
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
import { NavigationNodeRenderer } from '../ElementsTree/NavigationTreeNode/NavigationNodeRenderer';
import type { IElementsTreeCustomRenderer } from '../ElementsTree/useElementsTree';
@@ -24,8 +26,13 @@ const nestedStyles = css`
TreeNode {
margin-top: 8px;
&:only-child Control {
display: none;
&:only-child,
&[|hideProjects] {
margin-top: 0px;
& Control {
display: none;
}
}
& NavigationNodeNested {
@@ -57,7 +64,9 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
className,
expanded,
}) {
const elementsTreeContext = useContext(ElementsTreeContext);
const { node } = useNode(nodeId);
const hideProjects = elementsTreeContext?.tree.settings?.projects === false;
if (!node) {
return styled(TREE_NODE_STYLES)(
@@ -67,7 +76,7 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
);
}
return (
return styled(nestedStyles)(
<NavigationNodeRenderer
node={node}
path={path}
@@ -77,6 +86,7 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
control={NavigationNodeProjectControl}
style={nestedStyles}
component={component}
{...use({ hideProjects })}
/>
);
});
@@ -85,11 +85,11 @@ export class NavNodeContextMenuService extends Bootstrap {
? this.coreSettingsService.settings.getValue('app.metadata.editing')
: this.navTreeSettingsService.settings.getValue('editing');
if (!globalPermission) {
if (!globalPermission || !node.features) {
return false;
}
return !!node.features?.includes(ENodeFeature.canRename);
return node.features.includes(ENodeFeature.canRename);
}
if (action === ACTION_DELETE) {
@@ -97,11 +97,11 @@ export class NavNodeContextMenuService extends Bootstrap {
? this.coreSettingsService.settings.getValue('app.metadata.deleting')
: this.navTreeSettingsService.settings.getValue('deleting');
if (!globalPermission) {
if (!globalPermission || !node.features) {
return false;
}
return !!node.features?.includes(ENodeFeature.canDelete);
return node.features.includes(ENodeFeature.canDelete);
}
if (action === ACTION_OPEN) {
@@ -1,6 +1,7 @@
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/ElementsTreeTools/NavigationTreeSettings/ElementsTreeSettingsService';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl';
export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu';
@@ -17,9 +18,11 @@ export * from './NavigationTree/ElementsTree/NavNodeFilterCompareFn';
export * from './NavigationTree/ElementsTree/useElementsTree';
export * from './NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS';
export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService';
export * from './NavigationTree/ElementsTree/elementsTreeNameFilter';
export * from './NavigationTree/NavigationTreeBootstrap';
export * from './NavigationTree/NavigationTreeService';
export * from './NavigationTree/NavigationTree';
export * from './NavigationTree/getNavigationTreeUserSettingsId';
export * from './NodesManager/NavNodeView/IFolderTransform';
export * from './NodesManager/NavNodeView/NavNodeViewService';
export * from './NodesManager/NavNodeContextMenuService';
@@ -31,4 +31,6 @@ export default [
['app_navigationTree_connection_group_shared', 'Shared Connections'],
['app_navigationTree_empty_placeholder', 'No Connections.\nUse the top menu to setup connection to your database.'],
['app_navigationTree_drop_here', 'Drop here...'],
['plugin_navigation_tree_settings_projects_description', 'Displays elements under project groups'],
['plugin_navigation_tree_settings_projects_title', 'Group by Project'],
];
@@ -29,4 +29,6 @@ export default [
['app_navigationTree_connection_group_user', 'Connessioni Utente'],
['app_navigationTree_empty_placeholder', 'Nessuna Connessione.\nUsa il menù in altro per impostare una nuova connessione.'],
['app_navigationTree_drop_here', 'Drop here...'],
['plugin_navigation_tree_settings_projects_description', 'Displays elements under project groups'],
['plugin_navigation_tree_settings_projects_title', 'Group by Project'],
];
@@ -34,4 +34,6 @@ export default [
['app_navigationTree_connection_group_shared', 'Общие подключения'],
['app_navigationTree_empty_placeholder', 'Нет подключений.\nИспользуйте верхнее меню для создания подключения к базе.'],
['app_navigationTree_drop_here', 'Drop here...'],
['plugin_navigation_tree_settings_projects_description', 'Группировать элементы по проектам'],
['plugin_navigation_tree_settings_projects_title', 'Группировать по проектам'],
];
@@ -31,4 +31,6 @@ export default [
['app_navigationTree_connection_group_shared', '共享连接'],
['app_navigationTree_empty_placeholder', 'No Connections.\nUse the top menu to setup connection to your database.'],
['app_navigationTree_drop_here', 'Drop here...'],
['plugin_navigation_tree_settings_projects_description', 'Displays elements under project groups'],
['plugin_navigation_tree_settings_projects_title', 'Group by Project'],
];
@@ -10,6 +10,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di';
import { LocaleService } from './LocaleService';
import { ElementsTreeToolsMenuService } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService';
import { ElementsTreeSettingsService } from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/ElementsTreeSettingsService';
import { NavigationTreeBootstrap } from './NavigationTree/NavigationTreeBootstrap';
import { NavigationTreeService } from './NavigationTree/NavigationTreeService';
import { NavNodeContextMenuService } from './NodesManager/NavNodeContextMenuService';
@@ -25,5 +26,6 @@ export const navigationTreePlugin: PluginManifest = {
NavigationTreeBootstrap,
NavNodeContextMenuService,
NavNodeViewService,
ElementsTreeSettingsService,
],
};
@@ -0,0 +1,17 @@
# dependencies
/node_modules
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,47 @@
{
"name": "@cloudbeaver/plugin-projects",
"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-administration": "~0.1.0",
"@cloudbeaver/core-app": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-connections": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-dialogs": "~0.1.0",
"@cloudbeaver/core-events": "~0.1.0",
"@cloudbeaver/core-executor": "~0.1.0",
"@cloudbeaver/core-extensions": "~0.1.0",
"@cloudbeaver/core-localization": "~0.1.0",
"@cloudbeaver/core-notifications": "~0.1.0",
"@cloudbeaver/core-plugin": "~0.1.0",
"@cloudbeaver/core-product": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-routing": "~0.1.0",
"@cloudbeaver/core-sdk": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-theming": "~0.1.0",
"@cloudbeaver/core-ui": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0",
"@cloudbeaver/core-version-update": "~0.1.0",
"@cloudbeaver/core-view": "~0.1.0",
"@cloudbeaver/plugin-top-app-bar": "~0.1.0",
"mobx": "~6.x.x",
"mobx-react-lite": "~3.x.x",
"react": "~18.x.x",
"reshadow": "~0.x.x"
}
}
@@ -0,0 +1,186 @@
/*
* 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 { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { BASE_CONTAINERS_STYLES, Button, Container, InputField, SubmittingForm, useFocus, useObservableRef } from '@cloudbeaver/core-blocks';
import { CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs';
import { Translate, useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { throttleAsync } from '@cloudbeaver/core-utils';
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
const style = css`
footer {
align-items: center;
}
fill {
flex: 1;
}
`;
interface IFolderDialogState {
value: string;
projectId: string;
message: string | undefined;
valid: boolean;
payload: FolderDialogPayload;
validate: () => void;
setMessage: (message: string) => void;
setProjectId: (projectId: string) => void;
}
export interface IFolderDialogResult {
folder: string;
projectId: string;
}
export interface FolderDialogPayload {
value: string;
projectId: string;
selectProject: boolean;
objectName?: string;
icon?: string;
subTitle?: string;
bigIcon?: boolean;
viewBox?: string;
confirmActionText?: string;
create?: boolean;
title?: string;
validation?: (result: IFolderDialogResult, setMessage: (message: string) => void) => Promise<boolean> | boolean;
}
export const FolderDialog: DialogComponent<FolderDialogPayload, IFolderDialogResult> = observer(function FolderDialog({
payload,
resolveDialog,
rejectDialog,
className,
}) {
const translate = useTranslate();
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const {
icon,
subTitle,
bigIcon,
viewBox,
value,
projectId,
selectProject,
objectName,
create,
confirmActionText,
} = payload;
let { title } = payload;
if (!title) {
title = create ? 'ui_create' : 'ui_rename';
}
title = translate(title);
if (objectName) {
title += ` ${translate(objectName)}`;
}
const state = useObservableRef<IFolderDialogState>(() => ({
value,
projectId,
message: undefined,
valid: true,
validate: throttleAsync(async () => {
state.message = undefined;
state.valid = (await state.payload.validation?.(
{ folder:state.value, projectId: state.projectId },
state.setMessage.bind(state)
)) ?? true;
}, 300),
setMessage(message) {
this.message = message;
},
setProjectId(projectId) {
this.projectId = projectId;
},
}), {
value: observable.ref,
projectId: observable.ref,
valid: observable.ref,
message: observable.ref,
}, {
payload,
});
function resolveHandler() {
resolveDialog({ folder: state.value, projectId: state.projectId });
}
useEffect(() => {
state.validate();
}, [state.value, state.projectId]);
const errorMessage = state.valid ? ' ' : translate(state.message ?? 'ui_rename_taken_or_invalid');
return styled(useStyles(style, BASE_CONTAINERS_STYLES))(
<CommonDialogWrapper
size='small'
subTitle={subTitle}
title={title}
icon={icon}
viewBox={viewBox}
bigIcon={bigIcon}
className={className}
style={style}
footer={(
<>
<Button
type="button"
mod={['outlined']}
onClick={rejectDialog}
>
<Translate token='ui_processing_cancel' />
</Button>
<fill />
<Button
type="button"
mod={['unelevated']}
disabled={!state.valid}
onClick={resolveHandler}
>
<Translate token={confirmActionText || (create ? 'ui_create' : 'ui_rename')} />
</Button>
</>
)}
fixedWidth
onReject={rejectDialog}
>
<SubmittingForm ref={focusedRef} onSubmit={resolveHandler}>
<Container center gap>
{selectProject && (
<ProjectSelect
value={state.projectId}
onChange={projectId => state.setProjectId(projectId)}
/>
)}
<InputField
name='value'
state={state}
error={!state.valid}
description={errorMessage}
onChange={() => state.validate()}
>
{translate('ui_name') + ':'}
</InputField>
</Container>
</SubmittingForm>
</CommonDialogWrapper>
);
});
@@ -0,0 +1,36 @@
/*
* 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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { LocalizationService } from '@cloudbeaver/core-localization';
@injectable()
export class LocaleService extends Bootstrap {
constructor(private readonly localizationService: LocalizationService) {
super();
}
register(): void | Promise<void> {
this.localizationService.addProvider(this.provider.bind(this));
}
load(): void | Promise<void> { }
private async provider(locale: string) {
switch (locale) {
case 'ru':
return (await import('./locales/ru')).default;
case 'it':
return (await import('./locales/it')).default;
case 'zh':
return (await import('./locales/zh')).default;
default:
return (await import('./locales/en')).default;
}
}
}
@@ -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 { Bootstrap, injectable } from '@cloudbeaver/core-di';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
) {
super();
}
async load(): Promise<void> { }
register(): void {
}
}
@@ -15,37 +15,50 @@ import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
interface Props {
value: string | null;
filter?: (project: ProjectInfo) => boolean;
onChange: (value: string) => void;
autoHide?: boolean;
readOnly?: boolean;
disabled?: boolean;
inline?: boolean;
}
export const ProjectSelect = observer(function ProjectSelect(props: Props) {
export const ProjectSelect = observer(function ProjectSelect({
value,
filter = project => project.canEditDataSources,
autoHide,
readOnly,
disabled,
inline,
onChange,
}: Props) {
const translate = useTranslate();
const projectsService = useService(ProjectsService);
const projectsLoader = useMapResource(ProjectSelect, ProjectInfoResource, CachedMapAllKey, {
onData: () => {
if (!props.value && projectsService.activeProject) {
props.onChange(projectsService.activeProject.id);
if (!value && projectsService.activeProject) {
onChange(projectsService.activeProject.id);
}
},
});
const value = props.value ?? projectsService.activeProject?.id;
const projects = projectsLoader.data as ProjectInfo[];
const possibleOptions = projects.filter(project => project.canCreateConnections);
const possibleOptions = projects.filter(filter);
function handleProjectSelect(projectId: string) {
const project = projectsLoader.resource.get(projectId);
if (project?.canCreateConnections) {
props.onChange(projectId);
if (project && filter(project)) {
onChange(projectId);
}
}
if (autoHide && possibleOptions.length <= 1) {
return null;
}
return (
<Combobox
name='projectId'
@@ -54,17 +67,17 @@ export const ProjectSelect = observer(function ProjectSelect(props: Props) {
keySelector={project => project.id}
valueSelector={project => project.name}
titleSelector={project => project.description}
isDisabled={project => !project.canCreateConnections}
readOnly={props.readOnly || possibleOptions.length <= 1}
isDisabled={project => !project.canEditDataSources}
readOnly={readOnly || possibleOptions.length <= 1}
searchable={projects.length > 10}
disabled={props.disabled}
disabled={disabled}
loading={projectsLoader.isLoading()}
inline={props.inline}
inline={inline}
tiny
fill
onSelect={handleProjectSelect}
>
{translate('connections_connection_project')}
{translate('plugin_projects_project_select_label')}
</Combobox>
);
});
@@ -0,0 +1,5 @@
import { manifest } from './manifest';
export * from './FolderDialog';
export * from './ProjectSelect';
export default manifest;
@@ -0,0 +1,3 @@
export default [
['plugin_projects_project_select_label', 'Project'],
];
@@ -0,0 +1,3 @@
export default [
['plugin_projects_project_select_label', 'Project'],
];
@@ -0,0 +1,3 @@
export default [
['plugin_projects_project_select_label', 'Проект'],
];
@@ -0,0 +1,3 @@
export default [
['plugin_projects_project_select_label', 'Project'],
];
@@ -0,0 +1,23 @@
/*
* 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 { LocaleService } from './LocaleService';
import { PluginBootstrap } from './PluginBootstrap';
export const manifest: PluginManifest = {
info: {
name: 'Projects plugin',
},
providers: [
PluginBootstrap,
LocaleService,
],
};
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"composite": true
},
"include": ["src"],
"exclude": ["**/node_modules", "**/.*/"],
"references": [{ "path": "../*" }]
}
@@ -11,15 +11,17 @@ import { untracked } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { CONNECTION_FOLDER_NAME_VALIDATION } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { RenameDialog, DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs';
import { DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { NavTreeResource, NavNodeManagerService, NavNodeInfoResource, type INodeMoveData, navNodeMoveContext, getNodesFromContext, ENodeMoveType, type NavNode } from '@cloudbeaver/core-navigation-tree';
import { ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { createPath } from '@cloudbeaver/core-utils';
import { ActionService, MenuService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, type IElementsTree } from '@cloudbeaver/plugin-navigation-tree';
import { FolderDialog } from '@cloudbeaver/plugin-projects';
import { NAV_NODE_TYPE_RM_PROJECT } from '../NAV_NODE_TYPE_RM_PROJECT';
import { NavResourceNodeService } from '../NavResourceNodeService';
@@ -27,6 +29,15 @@ import { ResourceManagerResource } from '../ResourceManagerResource';
import { ResourceProjectsResource } from '../ResourceProjectsResource';
import { RESOURCES_NODE_PATH } from '../RESOURCES_NODE_PATH';
import { NAV_NODE_TYPE_RM_RESOURCE } from './NAV_NODE_TYPE_RM_RESOURCE';
import { ResourcesProjectsNavNodeService } from './ResourcesProjectsNavNodeService';
interface ITargetNode {
projectId: string;
folderId?: string;
projectNodeId: string;
selectProject: boolean;
}
@injectable()
export class ResourceFoldersBootstrap extends Bootstrap {
@@ -40,10 +51,12 @@ export class ResourceFoldersBootstrap extends Bootstrap {
private readonly navResourceNodeService: NavResourceNodeService,
private readonly resourceManagerResource: ResourceManagerResource,
private readonly resourceProjectsResource: ResourceProjectsResource,
private readonly projectInfoResource: ProjectInfoResource,
private readonly commonDialogService: CommonDialogService,
private readonly actionService: ActionService,
private readonly menuService: MenuService,
private readonly navNodeInfoResource: NavNodeInfoResource
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly resourcesProjectsNavNodeService: ResourcesProjectsNavNodeService
) {
super();
}
@@ -104,32 +117,25 @@ export class ResourceFoldersBootstrap extends Bootstrap {
const move = contexts.getContext(navNodeMoveContext);
const nodes = getNodesFromContext(moveContexts);
const nodeIdList = nodes.map(node => node.id);
const children = this.navTreeResource.get(targetNode.id);
const data = this.navResourceNodeService.getResourceData(targetNode.id);
const children = this.navTreeResource.get(targetNode.id) ?? [];
const targetProject = this.resourcesProjectsNavNodeService.getProject(targetNode.id);
if (!data) {
if (!targetProject?.canEditResources || (!targetNode.folder && targetNode.nodeType !== NAV_NODE_TYPE_RM_PROJECT)) {
return;
}
await this.resourceProjectsResource.load();
const projectPath = createPath(RESOURCES_NODE_PATH, this.resourceProjectsResource.userProject?.id);
const supported = nodes.every(node => {
if (
![NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE].includes(node.nodeType!)
|| targetProject !== this.resourcesProjectsNavNodeService.getProject(node.id)
|| children.includes(node.id)
|| targetNode.id === node.id
) {
return false;
}
if (!(this.resourceProjectsResource.userProject?.id === data.key.projectId)) {
return;
}
const supported = (
(
[NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE].includes(targetNode.nodeType!)
|| targetNode.id === projectPath
)
&& (targetNode.folder || NAV_NODE_TYPE_RM_PROJECT === targetNode.nodeType)
&& nodes.every(node => (
node.nodeType === NAV_NODE_TYPE_RM_RESOURCE
&& node.id !== targetNode.id
&& !children?.includes(node.id)
))
);
return true;
});
if (!supported) {
return;
@@ -140,7 +146,7 @@ export class ResourceFoldersBootstrap extends Bootstrap {
} else {
try {
await this.navTreeResource.moveTo(resourceKeyList(nodeIdList), targetNode.id);
await this.navTreeResource.refreshTree(RESOURCES_NODE_PATH);
await this.navTreeResource.refreshTree(RESOURCES_NODE_PATH, true);
} catch (exception: any) {
this.notificationService.logException(exception, 'plugin_resource_manager_folder_move_failed');
}
@@ -164,51 +170,49 @@ export class ResourceFoldersBootstrap extends Bootstrap {
return;
}
const folderData = this.navResourceNodeService.getResourceData(targetNode.id);
let parentFolder: string | undefined;
if (!folderData) {
return;
if (targetNode.folderId) {
const folderData = this.navResourceNodeService.getResourceData(targetNode.folderId);
if (folderData) {
parentFolder = folderData.resourcePath;
}
}
let { key } = folderData;
await this.resourceManagerResource.load({ projectId: targetNode.projectId, folder: parentFolder });
await this.resourceManagerResource.load(key);
const resourceData = this.resourceManagerResource.getResource(key, folderData.name);
if (resourceData?.folder) {
key = { ...key, folder: createPath(key.folder, resourceData.name) };
await this.resourceManagerResource.load(key);
}
const result = await this.commonDialogService.open(RenameDialog, {
const result = await this.commonDialogService.open(FolderDialog, {
value: this.localizationService.translate('ui_folder_new'),
projectId: targetNode.projectId,
title: 'core_view_action_new_folder',
subTitle: key.folder,
subTitle: parentFolder,
icon: '/icons/folder.svg#root',
create: true,
validation: (name, setMessage) => {
const trimmed = name.trim();
selectProject: targetNode.selectProject,
validation: async ({ folder, projectId }, setMessage) => {
const trimmed = folder.trim();
if (trimmed.length === 0 || !name.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
if (trimmed.length === 0 || !folder.match(CONNECTION_FOLDER_NAME_VALIDATION)) {
setMessage('connections_connection_folder_validation');
return false;
}
const folder = this.resourceManagerResource.getResource(key, trimmed);
await this.resourceManagerResource.load({ projectId: projectId, folder: parentFolder });
return folder === undefined;
return !this.resourceManagerResource.hasResource({ projectId: projectId, folder: parentFolder }, trimmed);
},
});
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
try {
await this.resourceManagerResource.createResource(
key.projectId,
createPath(key.folder, result),
result.projectId,
createPath(parentFolder, result.folder),
true
);
this.navTreeResource.refreshTree(targetNode.parentId);
this.navTreeResource.refreshTree(this.resourcesProjectsNavNodeService.getProjectNodeId(result.projectId));
} catch (exception: any) {
this.notificationService.logException(exception, 'Error occurred while renaming');
}
@@ -230,26 +234,51 @@ export class ResourceFoldersBootstrap extends Bootstrap {
}
}
private getTargetNode(tree: IElementsTree): NavNode | undefined {
private getTargetNode(tree: IElementsTree): ITargetNode | undefined {
untracked(() => this.projectInfoResource.load(CachedMapAllKey));
const selected = tree.getSelected();
if (selected.length === 0) {
selected.push(tree.root);
const editableProjects = this.projectInfoResource.values.filter(project => project.canEditResources);
if (editableProjects.length > 0) {
const project = editableProjects[0];
return {
projectId: project.id,
projectNodeId: this.resourcesProjectsNavNodeService.getProjectNodeId(project.id),
selectProject: editableProjects.length > 1,
};
}
return;
}
let targetFolder = selected[0];
let targetNode = this.navNodeInfoResource.get(targetFolder);
const targetFolder = selected[0];
const parentIds = [...this.navNodeInfoResource.getParents(targetFolder), targetFolder];
const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds));
const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_PROJECT);
if (![NAV_NODE_TYPE_RM_PROJECT, NAV_NODE_TYPE_RM_RESOURCE].includes(targetNode?.nodeType as any)) {
targetFolder = tree.baseRoot;
targetNode = this.navNodeInfoResource.get(targetFolder);
if (!projectNode) {
return;
}
const projectPath = createPath(RESOURCES_NODE_PATH, this.resourceProjectsResource.userProject?.id);
if (!targetNode?.id.startsWith(projectPath)) {
targetNode = undefined;
const project = this.resourcesProjectsNavNodeService.getByNodeId(projectNode.id);
if (!project?.canEditResources) {
return;
}
return targetNode;
const targetFolderNode = parents
.slice()
.reverse()
.find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_RESOURCE && parent.folder);
return {
projectId: project.id,
folderId: targetFolderNode?.id,
projectNodeId: projectNode.id,
selectProject: false,
};
}
}
@@ -0,0 +1,47 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { injectable } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { ProjectInfo, ProjectInfoResource } from '@cloudbeaver/core-projects';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import { createPath } from '@cloudbeaver/core-utils';
import { NAV_NODE_TYPE_RM_PROJECT } from '../NAV_NODE_TYPE_RM_PROJECT';
import { RESOURCES_NODE_PATH } from '../RESOURCES_NODE_PATH';
@injectable()
export class ResourcesProjectsNavNodeService {
constructor(
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly projectInfoResource: ProjectInfoResource
) {
}
getProject(nodeId: string): ProjectInfo | undefined {
const parentIds = [...this.navNodeInfoResource.getParents(nodeId), nodeId];
const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds));
const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_PROJECT);
if (!projectNode) {
return;
}
return this.getByNodeId(projectNode.id);
}
getByNodeId(nodeId: string): ProjectInfo | undefined {
return this.projectInfoResource.get(nodeId.replace(RESOURCES_NODE_PATH + '/', ''));
}
getProjectNodeId(projectId: string): string {
return createPath(RESOURCES_NODE_PATH, projectId);
}
}

Some files were not shown because too many files have changed in this diff Show More