diff --git a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf index 168e296f93..e4f986e684 100644 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf @@ -38,6 +38,7 @@ anonymousAccessEnabled: true, anonymousUserRole: "user", supportsCustomConnections: false, + showReadOnlyConnectionInfo: false, forwardProxy: false, diff --git a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf index a1f14be291..5dbc3c8d71 100644 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf @@ -38,6 +38,7 @@ anonymousAccessEnabled: true, anonymousUserRole: "user", supportsCustomConnections: false, + showReadOnlyConnectionInfo: false, forwardProxy: false, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index 7c757816f2..e395d82658 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -17,10 +17,12 @@ package io.cloudbeaver.model; import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.app.BaseWebAppConfiguration; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.service.sql.WebDataFormat; import io.cloudbeaver.utils.CBModelConstants; +import io.cloudbeaver.utils.WebAppUtils; import io.cloudbeaver.utils.WebCommonUtils; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.DBPDataSource; @@ -39,10 +41,7 @@ import org.jkiss.dbeaver.model.rm.RMProjectPermission; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.CommonUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -50,6 +49,7 @@ import java.util.stream.Collectors; */ public class WebConnectionInfo { + private static final String SECURED_VALUE = "********"; private final WebSession session; private final DBPDataSourceContainer dataSourceContainer; private WebServerError connectError; @@ -100,12 +100,18 @@ public class WebConnectionInfo { @Property public String getHost() { - return dataSourceContainer.getConnectionConfiguration().getHostName(); + if (canViewReadOnlyConnections()) { + return dataSourceContainer.getConnectionConfiguration().getHostName(); + } + return SECURED_VALUE; } @Property public String getPort() { - return dataSourceContainer.getConnectionConfiguration().getHostPort(); + if (canViewReadOnlyConnections()) { + return dataSourceContainer.getConnectionConfiguration().getHostPort(); + } + return SECURED_VALUE; } @Property @@ -115,17 +121,26 @@ public class WebConnectionInfo { @Property public String getDatabaseName() { - return dataSourceContainer.getConnectionConfiguration().getDatabaseName(); + if (canViewReadOnlyConnections()) { + return dataSourceContainer.getConnectionConfiguration().getDatabaseName(); + } + return SECURED_VALUE; } @Property public String getUrl() { - return dataSourceContainer.getConnectionConfiguration().getUrl(); + if (canViewReadOnlyConnections()) { + return dataSourceContainer.getConnectionConfiguration().getUrl(); + } + return SECURED_VALUE; } @Property public Map getProperties() { - return dataSourceContainer.getConnectionConfiguration().getProperties(); + if (canViewReadOnlyConnections()) { + return dataSourceContainer.getConnectionConfiguration().getProperties(); + } + return Collections.emptyMap(); } @Property @@ -352,7 +367,7 @@ public class WebConnectionInfo { @Property public boolean isCanViewSettings() { - return true; + return canViewReadOnlyConnections(); } @Property @@ -382,4 +397,13 @@ public class WebConnectionInfo { } return SMUtils.hasProjectPermission(session, ((WebProjectImpl) project).getRmProject(), projectPermission); } + + private boolean canViewReadOnlyConnections() { + if (isCanEdit()) { + return true; + } + BaseWebAppConfiguration appConfig = (BaseWebAppConfiguration) WebAppUtils.getWebApplication().getAppConfiguration(); + return appConfig.isShowReadOnlyConnectionInfo(); + + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java index 82a17315e5..7d02c127f5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebAppConfiguration.java @@ -31,6 +31,7 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration { protected final Map plugins; protected String defaultUserTeam; protected boolean resourceManagerEnabled; + protected boolean showReadOnlyConnectionInfo; protected String[] enabledFeatures; public BaseWebAppConfiguration() { @@ -38,6 +39,7 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration { this.defaultUserTeam = DEFAULT_APP_ANONYMOUS_TEAM_NAME; this.resourceManagerEnabled = true; this.enabledFeatures = null; + this.showReadOnlyConnectionInfo = false; } public BaseWebAppConfiguration(BaseWebAppConfiguration src) { @@ -45,6 +47,7 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration { this.defaultUserTeam = src.defaultUserTeam; this.resourceManagerEnabled = src.resourceManagerEnabled; this.enabledFeatures = src.enabledFeatures; + this.showReadOnlyConnectionInfo = src.showReadOnlyConnectionInfo; } @Override @@ -98,4 +101,8 @@ public abstract class BaseWebAppConfiguration implements WebAppConfiguration { public void setEnabledFeatures(String[] enabledFeatures) { this.enabledFeatures = enabledFeatures; } + + public boolean isShowReadOnlyConnectionInfo() { + return showReadOnlyConnectionInfo; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 5fa715498e..6463d8ccd0 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -155,6 +155,7 @@ public class LocalResourceController implements RMController { if (userProject != null) { projects.add(0, userProject); } + projects.sort(Comparator.comparing(RMProject::getDisplayName)); return projects.toArray(new RMProject[0]); } @@ -172,7 +173,6 @@ public class LocalResourceController implements RMController { RMProjectType.SHARED, true) ) .filter(Objects::nonNull) - .sorted(Comparator.comparing(RMProject::getDisplayName)) .collect(Collectors.toList()); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index 658249f951..d0803aeeda 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -64,6 +64,7 @@ import org.jkiss.dbeaver.model.runtime.ProxyProgressMonitor; import org.jkiss.dbeaver.model.security.*; import org.jkiss.dbeaver.model.security.user.SMObjectPermissions; import org.jkiss.dbeaver.model.sql.DBQuotaException; +import org.jkiss.dbeaver.registry.BaseProjectImpl; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; import org.jkiss.utils.CommonUtils; @@ -373,11 +374,7 @@ public class WebSession extends AbstractSessionPersistent RMController controller = getRmController(); RMProject[] rmProjects = controller.listAccessibleProjects(); for (RMProject project : rmProjects) { - WebProjectImpl virtualProject = createWebProject(project); - 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); - } + createWebProject(project); } if (user == null) { WebProjectImpl anonymousProject = createWebProject(RMUtils.createAnonymousProject()); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index 0269f7248e..c6105717e9 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -59,7 +59,7 @@ public class WebUserContext implements SMCredentialsProvider { public WebUserContext(WebApplication application) throws DBException { this.application = application; this.securityController = application.createSecurityController(this); - this.userPermissions = getDefaultPermissions(); + setUserPermissions(getDefaultPermissions()); } /** @@ -87,7 +87,7 @@ public class WebUserContext implements SMCredentialsProvider { authPermissions.getPermissions() ); this.refreshToken = smAuthInfo.getSmRefreshToken(); - this.userPermissions = authPermissions.getPermissions(); + setUserPermissions(authPermissions.getPermissions()); this.securityController = application.createSecurityController(this); this.adminSecurityController = application.getAdminSecurityController(this); this.secretController = application.getSecretController(this); @@ -122,7 +122,7 @@ public class WebUserContext implements SMCredentialsProvider { } catch (Exception e) { log.error("Error logging out user", e); } - this.userPermissions = getDefaultPermissions(); + setUserPermissions(getDefaultPermissions()); this.smCredentials = null; this.user = null; this.securityController = application.createSecurityController(this); @@ -171,11 +171,16 @@ public class WebUserContext implements SMCredentialsProvider { */ public synchronized void refreshPermissions() throws DBException { if (isAuthorizedInSecurityManager()) { - this.userPermissions = securityController.getTokenPermissions().getPermissions(); + setUserPermissions(securityController.getTokenPermissions().getPermissions()); } else { - this.userPermissions = getDefaultPermissions(); + setUserPermissions(getDefaultPermissions()); } - if (userPermissions != null && !userPermissions.isEmpty()) { + } + + private void setUserPermissions(Set permissions) { + this.userPermissions = permissions; + // FIXME: automatically assign public permission in sm controller˚ + if (!CommonUtils.isEmpty(userPermissions)) { userPermissions.add(DBWConstants.PERMISSION_PUBLIC); } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java similarity index 75% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java rename to server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java index 72a8f87879..3eb0ee5d74 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/DBWServiceServerConfigurator.java @@ -16,8 +16,8 @@ */ package io.cloudbeaver.service; +import io.cloudbeaver.model.app.WebAppConfiguration; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.CBAppConfig; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -28,8 +28,12 @@ import org.jkiss.dbeaver.model.app.DBPApplication; */ public interface DBWServiceServerConfigurator extends DBWServiceBinding { - void configureServer(@NotNull DBPApplication application, @Nullable WebSession session, @NotNull CBAppConfig appConfig) throws DBException; + void configureServer( + @NotNull DBPApplication application, + @Nullable WebSession session, + @NotNull WebAppConfiguration appConfig + ) throws DBException; - void reloadConfiguration(@NotNull CBAppConfig appConfig) throws DBException; + void reloadConfiguration(@NotNull WebAppConfiguration appConfig) throws DBException; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index e44699337e..3ab312a6fa 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -930,23 +930,25 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat rootConfig.put("app", appConfigProperties); copyConfigValue( - oldAppConfig, appConfigProperties, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); + oldAppConfig, appConfigProperties, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); copyConfigValue( - oldAppConfig, appConfigProperties, "supportsCustomConnections", appConfig.isSupportsCustomConnections()); + oldAppConfig, appConfigProperties, "supportsCustomConnections", appConfig.isSupportsCustomConnections()); copyConfigValue( - oldAppConfig, appConfigProperties, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); + oldAppConfig, appConfigProperties, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); copyConfigValue( - oldAppConfig, appConfigProperties, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); + oldAppConfig, appConfigProperties, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); copyConfigValue( - oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth()); + oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth()); copyConfigValue( - oldAppConfig, appConfigProperties, "forwardProxy", appConfig.isEnabledForwardProxy()); + oldAppConfig, appConfigProperties, "forwardProxy", appConfig.isEnabledForwardProxy()); copyConfigValue( - oldAppConfig, appConfigProperties, "linkExternalCredentialsWithUser", appConfig.isLinkExternalCredentialsWithUser()); + oldAppConfig, appConfigProperties, "linkExternalCredentialsWithUser", appConfig.isLinkExternalCredentialsWithUser()); copyConfigValue( - oldAppConfig, appConfigProperties, "redirectOnFederatedAuth", appConfig.isRedirectOnFederatedAuth()); + oldAppConfig, appConfigProperties, "redirectOnFederatedAuth", appConfig.isRedirectOnFederatedAuth()); copyConfigValue( - oldAppConfig, appConfigProperties, CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled()); + oldAppConfig, appConfigProperties, CBConstants.PARAM_RESOURCE_MANAGER_ENABLED, appConfig.isResourceManagerEnabled()); + copyConfigValue( + oldAppConfig, appConfigProperties, CBConstants.PARAM_SHOW_READ_ONLY_CONN_INFO, appConfig.isShowReadOnlyConnectionInfo()); Map resourceQuotas = new LinkedHashMap<>(); Map originResourceQuotas = JSONUtils.getObject(oldAppConfig, CBConstants.PARAM_RESOURCE_QUOTAS); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java index 5b2e04a638..b774d61635 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java @@ -39,6 +39,7 @@ public class CBConstants { public static final String PARAM_STATIC_CONTENT = "staticContent"; public static final String PARAM_RESOURCE_QUOTAS = "resourceQuotas"; public static final String PARAM_RESOURCE_MANAGER_ENABLED = "resourceManagerEnabled"; + public static final String PARAM_SHOW_READ_ONLY_CONN_INFO = "showReadOnlyConnectionInfo"; public static final String PARAM_AUTH_PROVIDERS = "authConfiguration"; public static final String PARAM_DB_CONFIGURATION = "database"; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java index 92cafe80aa..825b71203f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/CBDataSourceUpdatedEventHandlerImpl.java @@ -42,7 +42,7 @@ public class CBDataSourceUpdatedEventHandlerImpl extends CBProjectUpdatedEventHa CBEventConstants.EventType eventType = CBEventConstants.EventType.valueOf(JSONUtils.getString(event.getEventData(), "eventType")); WebProjectImpl project = activeUserSession.getProjectById(projectId); - if (project == null || !project.getRmProject().isShared()) { + if (project == null) { return; } activeUserSession.updateProjectConnection(project, dataSourceIds, eventType); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 767f3563ac..d36ac13c19 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -161,6 +161,9 @@ public class WebServiceNavigator implements DBWServiceNavigator { List nodeParents = new ArrayList<>(); for (DBNNode parent = node.getParentNode(); parent != null && !(parent instanceof DBNRoot); parent = parent.getParentNode()) { + if (parent instanceof DBNProjectDatabases) { + continue; + } nodeParents.add(new WebNavigatorNodeInfo(session, parent)); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 28d736399b..c510186d28 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -44,11 +44,11 @@ import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCTransaction; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.runtime.LoggingProgressMonitor; +import org.jkiss.dbeaver.model.security.*; import org.jkiss.dbeaver.model.security.exception.SMAccessTokenExpiredException; import org.jkiss.dbeaver.model.security.exception.SMException; import org.jkiss.dbeaver.model.security.exception.SMRefreshTokenExpiredException; import org.jkiss.dbeaver.model.security.user.*; -import org.jkiss.dbeaver.model.security.*; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; @@ -757,8 +757,11 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen dbStat.execute(); } - insertPermissions(dbCon, teamId, - new String[] {DBWConstants.PERMISSION_PUBLIC} , grantor); + insertPermissions(dbCon, + teamId, + getDefaultTeamPermissions(), + grantor + ); txn.commit(); } @@ -767,6 +770,10 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen } } + protected String[] getDefaultTeamPermissions() { + return new String[]{DBWConstants.PERMISSION_PUBLIC}; + } + @Override public void updateTeam(String teamId, String name, String description) throws DBCException { if (!isSubjectExists(teamId)) { @@ -1803,7 +1810,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen @Override public void updateSession(@NotNull String sessionId, @NotNull Map parameters) throws DBCException { - String userId = getUserIdOrThrow(); + String userId = getUserIdOrNull(); try (Connection dbCon = database.openConnection()) { try (PreparedStatement dbStat = dbCon.prepareStatement( "UPDATE CB_SESSION SET USER_ID=?,LAST_ACCESS_TIME=?,LAST_ACCESS_REMOTE_ADDRESS=?,LAST_ACCESS_USER_AGENT=?,LAST_ACCESS_INSTANCE_ID=? WHERE SESSION_ID=?")) { @@ -2156,10 +2163,21 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen return originalLink + "?authId=" + authId; } + + @NotNull private String getUserIdOrThrow() throws SMException { + String userId = getUserIdOrNull(); + if (userId == null) { + throw new SMException("User not authenticated"); + } + return userId; + } + + @Nullable + private String getUserIdOrNull() { SMCredentials activeUserCredentials = credentialsProvider.getActiveUserCredentials(); if (activeUserCredentials == null || activeUserCredentials.getUserId() == null) { - throw new SMException("User not authenticated"); + return null; } return activeUserCredentials.getUserId(); } diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts index 06b397d508..98c5043229 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts @@ -174,7 +174,7 @@ export class NavNodeManagerService extends Bootstrap { } async canOpen(nodeId: string, parentId: string, folderId?: string): Promise { - if (!this.navTree.has(nodeId)) { + if (!this.navNodeInfoResource.has(nodeId)) { return false; } diff --git a/webapp/packages/core-utils/src/isArrayEquals.test.ts b/webapp/packages/core-utils/src/isArrayEquals.test.ts index 32c4613b37..ba35107347 100644 --- a/webapp/packages/core-utils/src/isArrayEquals.test.ts +++ b/webapp/packages/core-utils/src/isArrayEquals.test.ts @@ -30,6 +30,30 @@ describe('Is array equals', () => { }); test('should use isEqual argument if passed', () => { - expect(isArraysEqual([{ a:1 }], [{ a: 1 }], (a, b) => a.a === b.a)).toBe(true); + expect(isArraysEqual([{ a: 1 }], [{ a: 1 }], (a, b) => a.a === b.a)).toBe(true); + }); + + test('should return "true" when arrays are the same and "order" argument is "true"', () => { + expect(isArraysEqual([1, '1', true], [1, '1', true], undefined, true)).toBe(true); + }); + + test('should return "false" when arrays are not the same and "order" argument is "true"', () => { + expect(isArraysEqual([1, '2', true], [2, '1', true], undefined, true)).toBe(false); + }); + + test('should return "false" when arrays have the same elements but elements order is different and "order" argument is "true"', () => { + expect(isArraysEqual(['1', 1, true], [1, '1', true], undefined, true)).toBe(false); + }); + + test('should return "false" when arrays length is different and the first array is larger and "order" argument is "true"', () => { + expect(isArraysEqual([1, true, '1'], [1, true], undefined, true)).toBe(false); + }); + + test('should return "false" when arrays length is different and the second array is larger and "order" argument is "true"', () => { + expect(isArraysEqual([1, true], [1, true, '1'], undefined, true)).toBe(false); + }); + + test('should use isEqual argument if passed and "order" argument is "true"', () => { + expect(isArraysEqual([{ a: 1 }], [{ a: 1 }], (a, b) => a.a === b.a, true)).toBe(true); }); }); \ No newline at end of file diff --git a/webapp/packages/core-utils/src/isArraysEqual.ts b/webapp/packages/core-utils/src/isArraysEqual.ts index 571ed8cc74..a35aa7a72a 100644 --- a/webapp/packages/core-utils/src/isArraysEqual.ts +++ b/webapp/packages/core-utils/src/isArraysEqual.ts @@ -11,11 +11,19 @@ const isObjectEqual = (a: T, b: T) => a === b; export function isArraysEqual( first: T[], second: T[], - isEqual: (a: T, b: T) => boolean = isObjectEqual + isEqual: (a: T, b: T) => boolean = isObjectEqual, + order?: boolean ): boolean { if (first.length !== second.length) { return false; } - return !first.some(b => !second.some(a => isEqual(a, b))); + + return !first.some((b, index) => { + if (order) { + return !isEqual(second[index], b); + } + + return !second.some(a => isEqual(a, b)); + }); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserForm.tsx index c5a88f7cd8..3a15ac737e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserForm.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserForm.tsx @@ -62,6 +62,7 @@ const topBarStyles = css` position: relative; display: flex; padding-top: 16px; + margin-bottom: 24px; &:before { content: ''; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx index 7dd1f357d4..68b2af96bf 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/UserInfo.tsx @@ -37,7 +37,7 @@ export const UserInfo: TabContainerPanelComponent = observer(fun ); return styled(style)( - + {translate('authentication_user_credentials')} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx new file mode 100644 index 0000000000..1ccb4e4b84 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/UsersTableFilters.tsx @@ -0,0 +1,115 @@ +/* + * 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 { useState } from 'react'; +import styled, { css, use } from 'reshadow'; + +import { AuthRolesResource } from '@cloudbeaver/core-authentication'; +import { BASE_CONTAINERS_STYLES, Combobox, Filter, Group, IconOrImage, useDataResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; + +import { IUserFilters, USER_ROLE_ALL, USER_STATUSES } from './useUsersTableFilters'; + +const styles = css` + filter-container { + display: flex; + gap: 12px; + } + + Filter { + flex: 1; + } + + actions { + composes: theme-form-element-radius theme-background-surface theme-text-on-surface theme theme-border-color-background from global; + box-sizing: border-box; + border: 2px solid; + height: 32px; + } + + button { + position: relative; + box-sizing: border-box; + background: inherit; + cursor: pointer; + width: 28px; + height: 100%; + padding: 4px; + + &:hover { + opacity: 0.8; + } + + &[|active] { + background: var(--theme-secondary); + } + } + + IconOrImage { + width: 100%; + height: 100%; + } +`; + +interface Props { + filters: IUserFilters; +} + +export const UsersTableFilters = observer(function UsersTableFilters({ filters }) { + const translate = useTranslate(); + const style = useStyles(BASE_CONTAINERS_STYLES, styles); + const authRolesResource = useDataResource(UsersTableFilters, AuthRolesResource, undefined); + + const [open, setOpen] = useState(false); + + return styled(style)( + + + + + + + + + {open && ( + + translate(value.label)} + keySelector={value => value.value} + keepSize + onSelect={filters.setStatus} + > + {translate('authentication_user_status')} + + {!!authRolesResource.data.length && ( + + {translate('authentication_user_role')} + + )} + + )} + + ); +}); \ No newline at end of file diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx new file mode 100644 index 0000000000..b471d3d58b --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/Filters/useUsersTableFilters.tsx @@ -0,0 +1,90 @@ +/* + * 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 { action, computed, observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; +import type { AdminUser } from '@cloudbeaver/core-authentication'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; + +export enum EUserStatus { + ENABLED = 'ENABLED', + DISABLED = 'DISABLED', + ALL = 'ALL' +} + +interface IStatus { + label: TLocalizationToken; + value: EUserStatus; +} + +export const USER_ROLE_ALL = 'ALL'; + +export const USER_STATUSES: IStatus[] = [ + { + label: 'authentication_administration_users_filters_status_enabled', + value: EUserStatus.ENABLED, + }, + { + label: 'authentication_administration_users_filters_status_disabled', + value: EUserStatus.DISABLED, + }, + { + label: 'authentication_administration_users_filters_status_all', + value: EUserStatus.ALL, + }, +]; + +export interface IUserFilters { + search: string; + role: string; + status: EUserStatus; + filteredUsers: AdminUser[]; + setSearch: (value: string) => void; + setRole: (role: string) => void; + setStatus: (status: EUserStatus) => void; +} + +export function useUsersTableFilters(users: AdminUser[]) { + const filters: IUserFilters = useObservableRef(() => ({ + search: '', + role: USER_ROLE_ALL, + status: EUserStatus.ENABLED, + get filteredUsers() { + return this.users.filter(user => { + const matchSearch = user.userId.toLowerCase().includes(this.search.trim().toLowerCase()); + const matchStatus = this.status === EUserStatus.ALL + || (this.status === EUserStatus.ENABLED ? user.enabled : !user.enabled); + const matchRole = this.role === USER_ROLE_ALL || this.role === user.authRole; + + return matchSearch && matchStatus && matchRole; + }) + }, + setSearch(value: string) { + this.search = value; + }, + setRole(role: string) { + this.role = role; + }, + setStatus(status: EUserStatus) { + this.status = status; + }, + }), { + search: observable.ref, + role: observable.ref, + status: observable.ref, + users: observable.ref, + filteredUsers: computed({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }), + setSearch: action.bound, + setRole: action.bound, + setStatus: action.bound, + }, { users }); + + return filters; +} \ No newline at end of file diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx index 9a2f65d48b..e6d65de2ab 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTable.tsx @@ -7,36 +7,24 @@ */ import { observer } from 'mobx-react-lite'; -import styled, { css, use } from 'reshadow'; +import styled, { css } from 'reshadow'; import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; -import { AdminUser, AuthRolesResource, UsersResource } from '@cloudbeaver/core-authentication'; -import { Table, TableHeader, TableColumnHeader, TableBody, TableSelect, useMapResource, ToolsAction, ToolsPanel, Loader, BASE_LAYOUT_GRID_STYLES, useTranslate, useStyles, useDataResource } from '@cloudbeaver/core-blocks'; -import { useController, useService } from '@cloudbeaver/core-di'; +import { AuthProvidersResource, AuthRolesResource, AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication'; +import { + Table, TableHeader, TableColumnHeader, TableBody, useMapResource, ToolsAction, + ToolsPanel, Loader, useTranslate, useStyles, useDataResource, + BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; -import { filterUndefined } from '@cloudbeaver/core-utils'; import { CreateUser } from './CreateUser'; import { CreateUserService } from './CreateUserService'; +import { UsersTableFilters } from './Filters/UsersTableFilters'; +import { useUsersTableFilters } from './Filters/useUsersTableFilters'; import { User } from './User'; -import { UsersTableController } from './UsersTableController'; - -const layoutStyles = css` - layout-grid { - width: 100%; - overflow: auto; - } - - layout-grid-inner { - min-height: 100%; - } - - layout-grid-cell { - composes: theme-background-surface theme-text-on-surface theme-border-color-background from global; - position: relative; - border: solid 1px; - } - `; +import { useUsersTable } from './useUsersTable'; const loaderStyle = css` ExceptionMessage { @@ -45,16 +33,36 @@ const loaderStyle = css` `; const styles = css` + ToolsPanel { + border-bottom: none; + border-radius: inherit; + } + + content { + display: flex; + flex-direction: column; + overflow: auto; + gap: 24px; + } + + Group { + padding: 0; + } + Table { width: 100%; } - [|table] { - min-height: 140px; /* loader overlay size */ + TableHeader { + background: var(--theme-surface); + position: sticky; + top: 0; + z-index: 1; } - - ToolsPanel { - border-bottom: none; + + table-container { + overflow: auto; + height: 100%; } `; @@ -65,53 +73,48 @@ interface Props { export const UsersTable = observer(function UsersTable({ sub, param }) { const translate = useTranslate(); - const style = useStyles(BASE_LAYOUT_GRID_STYLES, styles, ADMINISTRATION_TOOLS_PANEL_STYLES, layoutStyles); - const createUserService = useService(CreateUserService); - const controller = useController(UsersTableController); - const usersResource = useMapResource(UsersTable, UsersResource, CachedMapAllKey); - const authRoles = useDataResource(UsersTable, AuthRolesResource, undefined); - const isLocalProviderAvailable = controller.isLocalProviderAvailable; - const users = usersResource.resource.values - .filter(filterUndefined) - .sort((a, b) => { - if (usersResource.resource.isNew(a.userId) === usersResource.resource.isNew(b.userId)) { - return a.userId.localeCompare(b.userId); - } - if (usersResource.resource.isNew(a.userId)) { - return -1; - } - return 1; - }); + const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_CONTAINERS_STYLES); + const createUserService = useService(CreateUserService); + + const usersResource = useMapResource(UsersTable, UsersResource, CachedMapAllKey); + const authProvidersResource = useMapResource(UsersTable, AuthProvidersResource, CachedMapAllKey); + const authRolesResource = useDataResource(UsersTable, AuthRolesResource, undefined); + + const table = useUsersTable(usersResource.resource); + const filters = useUsersTableFilters(table.users); + + const isLocalProviderAvailable = authProvidersResource.resource.has(AUTH_PROVIDER_LOCAL_ID); const create = param === 'create'; - const keys = users.map(user => user.userId); - const displayAuthRole = authRoles.data.length > 0; + const displayAuthRole = authRolesResource.data.length > 0; + const keys = filters.filteredUsers.map(user => user.userId); return styled(style)( - - - - - {isLocalProviderAvailable && ( + + + + + + {isLocalProviderAvailable && ( + + {translate('ui_add')} + + )} - {translate('ui_add')} + {translate('ui_refresh')} - )} - - {translate('ui_refresh')} - - {/* {isLocalProviderAvailable && ( + {/* {isLocalProviderAvailable && ( (function UsersTable({ sub, param }) { {translate('ui_delete')} )} */} - - + + + + - {create && createUserService.user && ( - - - - )} - - - - - {/* {isLocalProviderAvailable && ( + + {create && createUserService.user && ( + + + + )} + + + +
+ + {/* {isLocalProviderAvailable && ( )} */} - - {translate('authentication_user_name')} - {displayAuthRole && ( - {translate('authentication_user_role')} - )} - {translate('authentication_user_team')} - {translate('authentication_user_enabled')} - - - - {users.map(user => ( - - ))} - -
-
-
- - + + {translate('authentication_user_name')} + {displayAuthRole && ( + {translate('authentication_user_role')} + )} + {translate('authentication_user_team')} + {translate('authentication_user_enabled')} + + + + {filters.filteredUsers.map(user => ( + + ))} + + + + + + +
); }); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableController.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableController.ts deleted file mode 100644 index cc9716e039..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersTableController.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2022 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import { observable, computed, makeObservable } from 'mobx'; - -import { AdminUser, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication'; -import { injectable, IInitializableController } from '@cloudbeaver/core-di'; -import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk'; - -@injectable() -export class UsersTableController implements IInitializableController { - isDeleting = false; - readonly selectedItems = observable(new Map()); - readonly expandedItems = observable(new Map()); - readonly error = new GQLErrorCatcher(); - - get users(): AdminUser[] { - return Array.from(this.usersResource.data.values()) - .sort((a, b) => { - if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) { - return a.userId.localeCompare(b.userId); - } - if (this.usersResource.isNew(a.userId)) { - return -1; - } - return 1; - }); - } - - get isProvidersLoading(): boolean { - return this.authProvidersResource.isLoading(); - } - - get isLocalProviderAvailable(): boolean { - return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); - } - - get isLoading(): boolean { - return this.usersResource.isLoading() || this.isDeleting; - } - - get itemsSelected(): boolean { - return Array.from(this.selectedItems.values()).some(v => v); - } - - constructor( - private notificationService: NotificationService, - private authProvidersResource: AuthProvidersResource, - private usersResource: UsersResource, - private commonDialogService: CommonDialogService, - private localizationService: LocalizationService - ) { - makeObservable(this, { - isDeleting: observable, - users: computed, - itemsSelected: computed, - }); - } - - init(): void { - this.authProvidersResource.loadAll(); - } - - update = async () => { - try { - await this.usersResource.refreshAll(); - this.notificationService.logSuccess({ title: 'authentication_administration_tools_refresh_success' }); - } catch (exception: any) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'Users update failed'); - } - } - }; - - delete = async () => { - if (this.isDeleting) { - return; - } - - const deletionList = Array - .from(this.selectedItems) - .filter(([_, value]) => value) - .map(([userId]) => userId); - if (deletionList.length === 0) { - return; - } - - const userNames = deletionList.map(name => `"${name}"`).join(', '); - const message = `${this.localizationService.translate('authentication_administration_users_delete_confirmation')}${userNames}. ${this.localizationService.translate('ui_are_you_sure')}`; - - const result = await this.commonDialogService.open(ConfirmationDialogDelete, { - title: 'ui_data_delete_confirmation', - message, - confirmActionText: 'ui_delete', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } - - this.isDeleting = true; - - try { - await this.usersResource.delete(resourceKeyList(deletionList)); - this.selectedItems.clear(); - - for (const id of deletionList) { - this.expandedItems.delete(id); - } - } catch (exception: any) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'User delete failed'); - } - } finally { - this.isDeleting = false; - } - }; - - showDetails = () => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - }; -} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx new file mode 100644 index 0000000000..256856fd59 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx @@ -0,0 +1,103 @@ +/* + * 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 { action, computed, observable } from 'mobx'; + +import type { AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; +import { TableState, useObservableRef, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; +import { isArraysEqual } from '@cloudbeaver/core-utils'; + +interface State { + loading: boolean; + state: TableState; + users: AdminUser[]; + update: () => Promise; + delete: () => Promise; +} + +export function useUsersTable(usersResource: UsersResource) { + const translate = useTranslate(); + const notificationService = useService(NotificationService); + const commonDialogService = useService(CommonDialogService); + + const state: State = useObservableRef(() => ({ + loading: false, + state: new TableState(), + get users() { + return this.usersResource.values + .slice() + .sort((a, b) => { + if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) { + return a.userId.localeCompare(b.userId); + } + if (this.usersResource.isNew(a.userId)) { + return -1; + } + return 1; + }); + }, + async update() { + try { + await this.usersResource.refreshAll(); + notificationService.logSuccess({ title: 'authentication_administration_tools_refresh_success' }); + } catch (exception: any) { + notificationService.logException(exception, 'authentication_administration_tools_refresh_fail'); + } + }, + async delete() { + if (this.loading) { + return; + } + + const deletionList = this.state.selectedList.filter(([_, value]) => value).map(([userId]) => userId); + if (deletionList.length === 0) { + return; + } + + const userNames = deletionList.map(name => `"${name}"`).join(', '); + const message = `${translate('authentication_administration_users_delete_confirmation')}${userNames}. ${translate('ui_are_you_sure')}`; + + const result = await commonDialogService.open(ConfirmationDialogDelete, { + title: 'ui_data_delete_confirmation', + message, + confirmActionText: 'ui_delete', + }); + + if (result === DialogueStateResult.Rejected) { + return; + } + + this.loading = true; + + try { + await this.usersResource.delete(resourceKeyList(deletionList)); + this.state.unselect(); + + for (const id of deletionList) { + this.state.unexpand(id); + } + } catch (exception: any) { + notificationService.logException(exception, 'authentication_administration_user_delete_fail'); + + } finally { + this.loading = false; + } + }, + }), { + loading: observable.ref, + users: computed({ equals: (first, second) => isArraysEqual(first, second, undefined, true) }), + update: action.bound, + delete: action.bound, + }, { usersResource }); + + return state; +} \ No newline at end of file diff --git a/webapp/packages/plugin-authentication-administration/src/locales/en.ts b/webapp/packages/plugin-authentication-administration/src/locales/en.ts index 93671c7e15..161e83ef8b 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/en.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/en.ts @@ -18,11 +18,17 @@ export default [ ['authentication_administration_tools_refresh_tooltip', 'Refresh user list'], ['authentication_administration_tools_delete_tooltip', 'Delete selected users'], ['authentication_administration_tools_refresh_success', 'User list was refreshed'], + ['authentication_administration_tools_refresh_fail', 'Users update failed'], + ['authentication_administration_user_delete_fail', 'Error deleting user'], ['authentication_administration_user_update_failed', 'Error saving user'], ['authentication_administration_user_updated', 'User updated'], ['authentication_administration_user_created', 'User was created successfully'], ['authentication_administration_user_create_failed', 'Error creating new user'], ['authentication_administration_users_delete_confirmation', "You're going to delete these users: "], + ['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'], + ['authentication_administration_users_filters_status_enabled', 'ENABLED'], + ['authentication_administration_users_filters_status_disabled', 'DISABLED'], + ['authentication_administration_users_filters_status_all', 'ALL'], ['administration_configuration_wizard_configuration_admin', 'Administrator Credentials'], ['administration_configuration_wizard_configuration_admin_name', 'Login'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/it.ts b/webapp/packages/plugin-authentication-administration/src/locales/it.ts index 7948205273..52c9cc25a0 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/it.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/it.ts @@ -15,10 +15,16 @@ export default [ ['authentication_administration_tools_refresh_tooltip', 'Aggiorna la lista utenti'], ['authentication_administration_tools_delete_tooltip', 'Elimina gli utenti selezionati'], ['authentication_administration_tools_refresh_success', 'Lista degli utenti ricaricata'], + ['authentication_administration_tools_refresh_fail', 'Users update failed'], + ['authentication_administration_user_delete_fail', 'Error deleting user'], ['authentication_administration_user_update_failed', 'Errore al salvataggio utente'], ['authentication_administration_user_updated', 'Utente aggiornato'], ['authentication_administration_user_created', 'Utente creato con successo'], ['authentication_administration_user_create_failed', 'Errore di creazione nuovo utente'], + ['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'], + ['authentication_administration_users_filters_status_enabled', 'ENABLED'], + ['authentication_administration_users_filters_status_disabled', 'DISABLED'], + ['authentication_administration_users_filters_status_all', 'ALL'], ['administration_teams_team_info_created', 'Team created'], ['administration_teams_team_info_updated', 'Team updated'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts index 966b36cbc2..e5828b9155 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/ru.ts @@ -18,11 +18,17 @@ export default [ ['authentication_administration_tools_refresh_tooltip', 'Обновить список пользователей'], ['authentication_administration_tools_delete_tooltip', 'Удалить выбранных пользователей'], ['authentication_administration_tools_refresh_success', 'Список пользователей обновлен'], + ['authentication_administration_tools_refresh_fail', 'Не удалось обновить список пользователей'], + ['authentication_administration_user_delete_fail', 'Не удалось удалить пользователя'], ['authentication_administration_user_update_failed', 'Не удалось обновить пользователя'], ['authentication_administration_user_updated', 'Пользователь обновлен'], ['authentication_administration_user_created', 'Пользователь создан успешно'], ['authentication_administration_user_create_failed', 'Не удалось создать пользователя'], ['authentication_administration_users_delete_confirmation', 'Вы собираетесь удалить следующих пользователей: '], + ['authentication_administration_users_filters_search_placeholder', 'Поиск по имени пользователя...'], + ['authentication_administration_users_filters_status_enabled', 'ВКЛЮЧЕН'], + ['authentication_administration_users_filters_status_disabled', 'ВЫКЛЮЧЕН'], + ['authentication_administration_users_filters_status_all', 'ВСЕ'], ['administration_configuration_wizard_configuration_admin', 'Учетные данные администратора'], ['administration_configuration_wizard_configuration_admin_name', 'Логин'], diff --git a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts index 773be62e07..8dd5eed4da 100644 --- a/webapp/packages/plugin-authentication-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-authentication-administration/src/locales/zh.ts @@ -17,11 +17,17 @@ export default [ ['authentication_administration_tools_refresh_tooltip', '刷新用户列表'], ['authentication_administration_tools_delete_tooltip', '删除选中的用户'], ['authentication_administration_tools_refresh_success', '用户列表已刷新'], + ['authentication_administration_tools_refresh_fail', 'Users update failed'], + ['authentication_administration_user_delete_fail', 'Error deleting user'], ['authentication_administration_user_update_failed', '保存用户出错'], ['authentication_administration_user_updated', '用户信息已更新'], ['authentication_administration_user_created', '创建用户成功'], ['authentication_administration_user_create_failed', '创建新用户出错'], ['authentication_administration_users_delete_confirmation', '您将删除这些用户:'], + ['authentication_administration_users_filters_search_placeholder', 'Search for the user name...'], + ['authentication_administration_users_filters_status_enabled', 'ENABLED'], + ['authentication_administration_users_filters_status_disabled', 'DISABLED'], + ['authentication_administration_users_filters_status_all', 'ALL'], ['administration_configuration_wizard_configuration_admin', '管理员凭据'], ['administration_configuration_wizard_configuration_admin_name', '登录'], diff --git a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx index b8caa50e86..f3a4e28e46 100644 --- a/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionAuthentication/NetworkHandlerAuthForm.tsx @@ -26,6 +26,7 @@ export const NetworkHandlerAuthForm = observer(function NetworkHandlerAut const translate = useTranslate(); const handler = useMapResource(NetworkHandlerAuthForm, NetworkHandlerResource, id); + //@TODO Do not mutate state in component body if (!networkHandlersConfig.some(state => state.id === id)) { networkHandlersConfig.push({ id, diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx index 6a157e2ad9..2699eb7beb 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx @@ -49,7 +49,6 @@ export const DBAuthDialogFooter = observer>(funct type="button" mod={['unelevated']} disabled={isAuthenticating} - loading={isAuthenticating} onClick={onLogin} > {translate('authentication_login')} diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index d7a0708583..93b6e10305 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -15,6 +15,7 @@ import { useFocus, ErrorMessage, useStyles, + Loader, } from '@cloudbeaver/core-blocks'; import { IConnectionInfoParams, useConnectionInfo, useDBDriver } from '@cloudbeaver/core-connections'; import { useController } from '@cloudbeaver/core-di'; @@ -86,20 +87,20 @@ export const DatabaseAuthDialog: DialogComponent = observer(function Da )} onReject={options?.persistent ? undefined : rejectDialog} > - - + + {!connection.isLoaded() || connection.isLoading() || !controller.configured + ? + : ( + + )} ); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts index 0d37cdafde..f68136b500 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-resource/src/PluginBootstrap.ts @@ -191,9 +191,9 @@ export class PluginBootstrap extends Bootstrap { try { const nodeInfo = contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); - const node = await this.navNodeInfoResource.load(data.nodeId); + const node = this.navNodeInfoResource.get(data.nodeId); - if (node.nodeType !== RESOURCE_NODE_TYPE || !isScript(node.id)) { + if (!node || node.nodeType !== RESOURCE_NODE_TYPE || !isScript(node.id)) { return; } diff --git a/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/topMenuStyles.ts b/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/topMenuStyles.ts index f56fb47f9a..8ab9438dba 100644 --- a/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/topMenuStyles.ts +++ b/webapp/packages/plugin-top-app-bar/src/TopNavBar/shared/topMenuStyles.ts @@ -129,7 +129,7 @@ export const topMenuStyles = css` display: none; } } - MenuButton, MenuBarElement { + MenuBarElement { &:hover, &:global([aria-expanded="true"]) { background: #338ecc; } diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index 391e924033..a5029ce2a6 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -1,7 +1,7 @@ { "name": "@cloudbeaver/product-default", "sideEffects": false, - "version": "22.2.4", + "version": "22.3.0", "description": "", "license": "Apache-2.0", "main": "lib/index.js", @@ -22,8 +22,6 @@ "@cloudbeaver/core-bootstrap": "0.1.0", "@cloudbeaver/core-di": "0.1.0", "@cloudbeaver/core-product": "0.1.0", - "@cloudbeaver/plugin-user-profile": "0.1.0", - "@cloudbeaver/plugin-help": "0.1.0", "@cloudbeaver/plugin-administration": "0.1.0", "@cloudbeaver/plugin-authentication": "0.1.0", "@cloudbeaver/plugin-authentication-administration": "0.1.0", @@ -36,6 +34,7 @@ "@cloudbeaver/plugin-data-viewer": "0.1.0", "@cloudbeaver/plugin-datasource-context-switch": "0.1.0", "@cloudbeaver/plugin-ddl-viewer": "0.1.0", + "@cloudbeaver/plugin-help": "0.1.0", "@cloudbeaver/plugin-localization": "0.1.0", "@cloudbeaver/plugin-log-viewer": "0.1.0", "@cloudbeaver/plugin-navigation-tree": "0.1.0", @@ -48,6 +47,7 @@ "@cloudbeaver/plugin-sql-editor-screen": "0.1.0", "@cloudbeaver/plugin-theme": "0.1.0", "@cloudbeaver/plugin-top-app-bar": "0.1.0", + "@cloudbeaver/plugin-user-profile": "0.1.0", "@cloudbeaver/plugin-version": "0.1.0", "@cloudbeaver/plugin-version-update-administration": "0.1.0" },