mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-13 07:47:05 +08:00
Merge pull request #1144 from dbeaver/CB-2406-projects-permission-managment
CB-2406 rm project permissions api
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+33
-13
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-2
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
+8
-8
@@ -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,
|
||||
|
||||
+1
-2
@@ -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) {
|
||||
|
||||
+2
-3
@@ -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!
|
||||
|
||||
+3
-11
@@ -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!
|
||||
|
||||
}
|
||||
+31
-1
@@ -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;
|
||||
}
|
||||
|
||||
+15
@@ -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"))
|
||||
))
|
||||
;
|
||||
|
||||
}
|
||||
|
||||
+62
-1
@@ -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();
|
||||
}
|
||||
|
||||
+21
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+1
-1
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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: () => {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+2
-3
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+51
-38
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
+5
-2
@@ -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}
|
||||
|
||||
+78
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
+28
@@ -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);
|
||||
}
|
||||
}
|
||||
-1
@@ -8,7 +8,6 @@
|
||||
|
||||
import { createKeyBinding } from '@cloudbeaver/core-view';
|
||||
|
||||
|
||||
export const KEY_BINDING_ENABLE_FILTER = createKeyBinding({
|
||||
id: 'enable-filter',
|
||||
label: 'Ctrl + F',
|
||||
|
||||
+15
-56
@@ -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>
|
||||
);
|
||||
|
||||
+2
@@ -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'
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+6
@@ -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}
|
||||
|
||||
+51
@@ -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,
|
||||
};
|
||||
+21
@@ -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);
|
||||
};
|
||||
+14
-4
@@ -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 })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
+4
-4
@@ -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 {
|
||||
}
|
||||
}
|
||||
+25
-12
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+47
@@ -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
Reference in New Issue
Block a user