diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 562bfcb127..184356576e 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -7,3 +7,4 @@ services: - "8978:8978" volumes: - "./logs:/opt/cloudbeaver/logs" + - "./workspace:/opt/cloudbeaver/workspace" diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 039ebb7ff2..d79a6d4dd7 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -303,6 +303,9 @@ extend type Mutation { # Refreshes session on server and returns its state touchSession: Boolean + # Refresh session connection list + refreshSessionConnections: Boolean + # Refreshes session on server and returns its state changeSessionLanguage(locale: String): Boolean diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java index 014b37311d..85f1380ae7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java @@ -16,6 +16,8 @@ */ package io.cloudbeaver.model.session; +import io.cloudbeaver.DBWConnectionGrant; +import io.cloudbeaver.DBWConstants; import io.cloudbeaver.DBWSecurityController; import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.WebAsyncTaskInfo; @@ -31,14 +33,12 @@ import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.access.DBASession; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.meta.Association; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.navigator.DBNModel; -import org.jkiss.dbeaver.model.navigator.DBNNode; -import org.jkiss.dbeaver.model.navigator.DBNProject; -import org.jkiss.dbeaver.model.navigator.DBNProjectDatabases; +import org.jkiss.dbeaver.model.navigator.*; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.BaseProgressMonitor; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -52,6 +52,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.stream.Collectors; /** * Web session. @@ -74,6 +75,8 @@ public class WebSession implements DBASession { private WebUser user; private Set sessionPermissions = null; + private Set accessibleConnectionIds = Collections.emptySet(); + private String locale; private boolean cacheExpired; @@ -153,7 +156,11 @@ public class WebSession implements DBASession { return user; } - public synchronized Set getSessionPermissions() throws DBCException { + public synchronized boolean hasPermission(String perm) { + return getSessionPermissions().contains(perm); + } + + public synchronized Set getSessionPermissions() { if (sessionPermissions == null) { refreshSessionAuth(); } @@ -166,22 +173,22 @@ public class WebSession implements DBASession { } this.user = user; + refreshSessionAuth(); + try { resetSessionCache(); initNavigatorModel(); } catch (DBCException e) { log.error(e); } - try { - refreshSessionAuth(); - } catch (DBCException e) { - log.error(e); - } } private void initNavigatorModel() { CBPlatform platform = CBPlatform.getInstance(); this.navigatorModel = new DBNModel(platform, this); + // Add datasource filter (based on permissions) + navigatorModel.addFilter(node -> + !(node instanceof DBNDataSource) || isDataSourceAccessible(((DBNDataSource)node).getDataSourceContainer())); this.navigatorModel.initialize(); DBPProject project = platform.getWorkspace().getActiveProject(); @@ -189,17 +196,66 @@ public class WebSession implements DBASession { this.databases = projectNode.getDatabases(); this.locale = Locale.getDefault().getLanguage(); + try { + this.refreshConnections(); + } catch (Exception e) { + log.error("Error getting connection list", e); + } + } + + public void refreshConnections() { + + // Add all provided datasources to the session + List connList = new ArrayList<>(); + DBPDataSourceRegistry registry = databases.getDataSourceRegistry(); + registry.refreshConfig(); + + for (DBPDataSourceContainer ds : registry.getDataSources()) { + if (ds.isProvided()) { + WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds); + connList.add(connectionInfo); + } + } + filterAccessibleConnections(connList); + // Add all provided datasources to the session synchronized (connections) { - for (DBPDataSourceContainer ds : databases.getDataSourceRegistry().getDataSources()) { - if (ds.isProvided()) { - WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds); - connections.put(connectionInfo.getId(), connectionInfo); - } + connections.clear(); + for (WebConnectionInfo connectionInfo : connList) { + connections.put(connectionInfo.getId(), connectionInfo); } } } + public void filterAccessibleConnections(List connections) { + if (this.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + // All connections are accessible + return; + } + connections.removeIf(c -> !accessibleConnectionIds.contains(c.getId())); + } + + private boolean isDataSourceAccessible(DBPDataSourceContainer dataSource) { + return this.hasPermission(DBWConstants.PERMISSION_ADMIN) || + accessibleConnectionIds.contains(dataSource.getId()); + } + + @NotNull + private Set readAccessibleConnectionIds() { + CBApplication application = CBApplication.getInstance(); + String subjectId = user == null ? + application.getAppConfiguration().getAnonymousUserRole() : user.getUserId(); + + try { + return Arrays.stream(application.getSecurityController() + .getSubjectConnectionAccess(new String[]{subjectId})) + .map(DBWConnectionGrant::getConnectionId).collect(Collectors.toSet()); + } catch (DBCException e) { + log.error("Error reading connection grants", e); + return Collections.emptySet(); + } + } + private void resetSessionCache() throws DBCException { // Clear attributes synchronized (attributes) { @@ -230,17 +286,25 @@ public class WebSession implements DBASession { this.databases = null; } - private void refreshSessionAuth() throws DBCException { - CBApplication application = CBPlatform.getInstance().getApplication(); - if (this.user == null) { - if (application.getAppConfiguration().isAnonymousAccessEnabled()) { - sessionPermissions = application.getSecurityController().getSubjectPermissions( - application.getAppConfiguration().getAnonymousUserRole()); + private void refreshSessionAuth() { + try { + CBApplication application = CBPlatform.getInstance().getApplication(); + + if (this.user == null) { + if (application.getAppConfiguration().isAnonymousAccessEnabled()) { + sessionPermissions = application.getSecurityController().getSubjectPermissions( + application.getAppConfiguration().getAnonymousUserRole()); + } else { + sessionPermissions = Collections.emptySet(); + } } else { - sessionPermissions = Collections.emptySet(); + sessionPermissions = application.getSecurityController().getUserPermissions(this.user.getUserId()); } - } else { - sessionPermissions = application.getSecurityController().getUserPermissions(this.user.getUserId()); + + accessibleConnectionIds = readAccessibleConnectionIds(); + + } catch (Exception e) { + log.error("Error reading session permissions", e); } } 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 0f92aa77df..55af1ba502 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 @@ -20,7 +20,9 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import com.google.gson.stream.JsonWriter; +import io.cloudbeaver.DBWConnectionGrant; import io.cloudbeaver.DBWSecurityController; +import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.server.jetty.CBJettyServer; import org.eclipse.core.runtime.Platform; import org.eclipse.equinox.app.IApplicationContext; @@ -29,6 +31,7 @@ import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.ModelPreferences; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.app.DBPApplication; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; @@ -38,6 +41,7 @@ import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.dbeaver.utils.PrefUtils; import org.jkiss.dbeaver.utils.SystemVariablesResolver; +import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.StandardConstants; @@ -407,6 +411,11 @@ public class CBApplication extends BaseApplicationImpl { log.debug("Saving runtime configuration"); saveRuntimeConfig(newServerName, appConfig); + // Grant permissions to predefined connections + if (appConfig.isAnonymousAccessEnabled()) { + grantAnonymousAccessToConnections(appConfig, adminName); + } + // Re-load runtime configuration try { log.debug("Reloading application configuration"); @@ -422,6 +431,24 @@ public class CBApplication extends BaseApplicationImpl { configurationMode = CommonUtils.isEmpty(serverName); } + private void grantAnonymousAccessToConnections(CBAppConfig appConfig, String adminName) { + try { + String anonymousRoleId = appConfig.getAnonymousUserRole(); + DBWSecurityController securityController = getSecurityController(); + for (DBPDataSourceContainer ds : WebServiceUtils.getDataSourceRegistry().getDataSources()) { + DBWConnectionGrant[] grants = securityController.getConnectionSubjectAccess(ds.getId()); + if (ArrayUtils.isEmpty(grants)) { + securityController.setConnectionSubjectAccess( + ds.getId(), + new String[] { anonymousRoleId }, + adminName); + } + } + } catch (Exception e) { + log.error("Error granting anonymous access to connections", e); + } + } + private void saveRuntimeConfig(String newServerName, CBAppConfig appConfig) throws DBException { File runtimeConfigFile = getRuntimeConfigFile(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 19a262b0c1..1affc3045f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -28,7 +28,6 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.graphql.GraphQLEndpoint; -import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.utils.CommonUtils; import javax.servlet.http.HttpServletRequest; @@ -147,12 +146,7 @@ public abstract class WebServiceBindingBase impleme } if (!CBApplication.getInstance().isConfigurationMode()) { // Check permissions - Set sessionPermissions; - try { - sessionPermissions = session.getSessionPermissions(); - } catch (DBCException e) { - throw new DBWebExceptionAccessDenied("Can't retrieve session permissions", e); - } + Set sessionPermissions = session.getSessionPermissions(); if (CommonUtils.isEmpty(sessionPermissions)) { throw new DBWebExceptionAccessDenied("Anonymous access restricted"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index f19acfc11c..c399bcd87b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -69,6 +69,9 @@ public interface DBWServiceCore extends DBWService { @WebAction(requirePermissions = {}) boolean touchSession(HttpServletRequest request) throws DBWebException; + @WebAction(requirePermissions = {}) + boolean refreshSessionConnections(HttpServletRequest request) throws DBWebException; + @WebAction boolean changeSessionLanguage(WebSession webSession, String locale) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 87f3f370cd..8a42268630 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -66,6 +66,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase .dataFetcher("openSession", env -> getService(env).openSession(sessionManager.getWebSession(GraphQLEndpoint.getServletRequest(env), false))) .dataFetcher("closeSession", env -> getService(env).closeSession(GraphQLEndpoint.getServletRequest(env))) .dataFetcher("touchSession", env -> getService(env).touchSession(GraphQLEndpoint.getServletRequest(env))) + .dataFetcher("refreshSessionConnections", env -> getService(env).refreshSessionConnections(GraphQLEndpoint.getServletRequest(env))) .dataFetcher("changeSessionLanguage", env -> getService(env).changeSessionLanguage(getWebSession(env), env.getArgument("locale"))) .dataFetcher("openConnection", env -> getService(env).openConnection(getWebSession(env), getConnectionConfig(env))) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 79092f8208..767edbc914 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -33,7 +33,6 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.data.json.JSONUtils; -import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.registry.DataSourceDescriptor; @@ -101,10 +100,13 @@ public class WebServiceCore implements DBWServiceCore { public List getTemplateConnections(WebSession webSession) throws DBWebException { List result = new ArrayList<>(); for (DBPDataSourceContainer ds : WebServiceUtils.getDataSourceRegistry().getDataSources()) { - if (ds.isTemplate() && CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) { + if (ds.isTemplate() && + CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) + { result.add(new WebConnectionInfo(webSession, ds)); } } + webSession.filterAccessibleConnections(result); return result; } @@ -117,11 +119,7 @@ public class WebServiceCore implements DBWServiceCore { DBWConstants.PERMISSION_ADMIN }; } - try { - return webSession.getSessionPermissions().toArray(new String[0]); - } catch (DBCException e) { - throw new DBWebException("Error reading session permissions", e); - } + return webSession.getSessionPermissions().toArray(new String[0]); } @Override @@ -149,6 +147,17 @@ public class WebServiceCore implements DBWServiceCore { return CBPlatform.getInstance().getSessionManager().touchSession(request); } + @Override + public boolean refreshSessionConnections(HttpServletRequest request) throws DBWebException { + WebSession session = CBPlatform.getInstance().getSessionManager().getWebSession(request); + if (session == null) { + return false; + } else { + session.refreshConnections(); + return true; + } + } + @Override public boolean changeSessionLanguage(WebSession webSession, String locale) { webSession.setLocale(locale); diff --git a/webapp/packages/core-administration/src/Administration/ItemContent.tsx b/webapp/packages/core-administration/src/Administration/ItemContent.tsx index 3b47d6e722..18346d6e77 100644 --- a/webapp/packages/core-administration/src/Administration/ItemContent.tsx +++ b/webapp/packages/core-administration/src/Administration/ItemContent.tsx @@ -40,7 +40,7 @@ export const ItemContent = observer(function ItemContent({ if (activeItemSub) { const sub = administrationItemService.getItemSub(item, activeItemSub); if (sub) { - const Component = sub.getComponent(); + const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent(); return ; } diff --git a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts index 228f4971ec..98313809a4 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -49,7 +49,7 @@ export type AdministrationItemSubCanActivateEvent = ( export interface IAdministrationItemSubItem { name: string; - getComponent(): AdministrationItemSubContentComponent; + getComponent?: () => AdministrationItemSubContentComponent; onActivate?: AdministrationItemSubEvent; canActivate?: AdministrationItemSubCanActivateEvent; } diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 06533dbc53..ed98975f4a 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -9,6 +9,7 @@ import { computed, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; +import { IExecutor, Executor } from '@cloudbeaver/core-executor'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { ScreenService, RouterService } from '@cloudbeaver/core-routing'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; @@ -57,6 +58,7 @@ export class AdministrationScreenService { get isConfigurationMode() { return !!this.serverConfigResource.data?.configurationMode; } + readonly activationEvent: IExecutor; constructor( private screenService: ScreenService, @@ -66,6 +68,7 @@ export class AdministrationScreenService { private serverConfigResource: ServerConfigResource ) { this.itemState = new Map(); + this.activationEvent = new Executor(); this.autoSaveService.withAutoSave(this.itemState, ADMINISTRATION_ITEMS_STATE); } @@ -124,7 +127,7 @@ export class AdministrationScreenService { this.itemState.clear(); } - private isAdministrationRouteActive() { + isAdministrationRouteActive() { return this.screenService.isActive(AdministrationScreenService.screenName) || this.screenService.isActive(AdministrationScreenService.setupName); } diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenServiceBootstrap.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenServiceBootstrap.ts index b93f5d3477..71d4029999 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenServiceBootstrap.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenServiceBootstrap.ts @@ -57,6 +57,7 @@ export class AdministrationScreenServiceBootstrap extends Bootstrap { ], component: AdministrationScreen, onActivate: this.handleActivate.bind(this), + onDeactivate: this.handleDeactivate.bind(this), }); this.screenService.create({ @@ -99,6 +100,10 @@ export class AdministrationScreenServiceBootstrap extends Bootstrap { } private async handleDeactivate() { + if (!this.administrationScreenService.isAdministrationRouteActive()) { + this.administrationScreenService.activationEvent.execute(false); + } + if (this.administrationScreenService.isConfigurationMode && !this.screenService.isActive(AdministrationScreenService.setupName)) { this.administrationScreenService.navigateToRoot(); @@ -122,6 +127,7 @@ export class AdministrationScreenServiceBootstrap extends Bootstrap { if (!await this.checkPermissions()) { return; } + this.administrationScreenService.activationEvent.execute(true); if (this.administrationScreenService.activeItem) { await this.administrationItemService.activate( diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/Finish/FinishPage.tsx b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/Finish/FinishPage.tsx index c19974014d..0e851d08ae 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/Finish/FinishPage.tsx +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/Finish/FinishPage.tsx @@ -41,6 +41,11 @@ const styles = composes( border: solid 1px; padding: 16px 24px } + + p { + line-height: 2; + white-space: pre; + } ` ); @@ -52,8 +57,8 @@ export const FinishPage = observer(function FinishPage() { -

All done

-

Go to fun

+

{translate('administration_configuration_wizard_finish_title')}

+

{translate('administration_configuration_wizard_finish_message')}

} + {onAdd && } diff --git a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx index 6883c5ad8c..d4251ee281 100644 --- a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx +++ b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx @@ -134,10 +134,10 @@ export const PropertyItem = observer(function PropertyItem({ const handleRemove = useCallback(() => onRemove(property.id), [property]); useLayoutEffect(() => { - if (keyInputRef.current && isKeyEditable) { + if (keyInputRef.current && isKeyEditable && property.new) { keyInputRef.current.focus(); } - }, []); + }, [property]); return styled(useStyles(styles))( diff --git a/webapp/packages/core-bootstrap/src/AppBootstrap.ts b/webapp/packages/core-bootstrap/src/AppBootstrap.ts index 1ac408b8ed..17546199db 100644 --- a/webapp/packages/core-bootstrap/src/AppBootstrap.ts +++ b/webapp/packages/core-bootstrap/src/AppBootstrap.ts @@ -12,12 +12,10 @@ import { NavigationTreeContextMenuService, LogViewerMenuService, ConnectionSchemaManagerService, - AppScreenService, } from '@cloudbeaver/core-app'; import { injectable } from '@cloudbeaver/core-di'; import { SessionExpireService } from '@cloudbeaver/core-dialogs'; import { ExceptionsCatcherService } from '@cloudbeaver/core-events'; -import { RouterService } from '@cloudbeaver/core-routing'; import { ThemeService } from '@cloudbeaver/core-theming'; /** diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx index 9049b630e9..004588e5bb 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverProperties.tsx @@ -39,7 +39,7 @@ export const DriverProperties = observer(function DriverProperties({ state, loadProperties, }: DriverPropertiesProps) { - const controller = useController(DriverPropertiesController, driverId); + const controller = useController(DriverPropertiesController, driverId, state); useMemo(() => { if (loadProperties) { @@ -54,7 +54,7 @@ export const DriverProperties = observer(function DriverProperties({ )} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverPropertiesController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverPropertiesController.ts index 2d962886c0..e467d10628 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverPropertiesController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/DriverProperties/DriverPropertiesController.ts @@ -32,20 +32,24 @@ export class DriverPropertiesController implements IInitializableController { loaded = false; + private state!: Record; + constructor( private driverPropertiesService: DriverPropertiesService, private notificationService: NotificationService ) { } - init(driverId: string) { + init(driverId: string, state: Record) { this.driverId = driverId; + this.state = state; } - onAddProperty = () => { + addProperty = (key?: string, value?: string) => { this.driverProperties.unshift({ id: uuid(), - key: 'property', - defaultValue: '', + key: key ?? 'property', + defaultValue: value ?? '', + new: !key, }); } @@ -64,6 +68,14 @@ export class DriverPropertiesController implements IInitializableController { description: property.description, validValues: property.validValues, }))); + + for (const key of Object.keys(this.state)) { + if (this.driverProperties.some(property => property.key === key)) { + continue; + } + + this.addProperty(key, this.state[key]); + } this.loaded = true; } catch (exception) { this.notificationService.logException(exception, 'Can\'t load driver properties'); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts index 69e135f8d2..e245f847bc 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionForm/Options/OptionsController.ts @@ -100,7 +100,7 @@ implements IInitializableController { const prevDriver = this.dbDriverResource.get(prevDriverId || ''); if (this.model.connection.host === prevDriver?.defaultServer) { - this.model.connection.host = this.driver?.defaultServer; + this.model.connection.host = this.driver?.defaultServer || 'localhost'; } if (this.model.connection.port === prevDriver?.defaultPort) { diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx index 782baa80b4..66b59ab3da 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministration.tsx @@ -12,6 +12,7 @@ import styled, { css, use } from 'reshadow'; import { AdministrationTools, AdministrationItemContentProps } from '@cloudbeaver/core-administration'; import { Loader, IconButton } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; +import { Translate } from '@cloudbeaver/core-localization'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import { ConnectionsAdministrationController } from './ConnectionsAdministrationController'; @@ -24,11 +25,16 @@ const styles = composes( composes: theme-background-surface theme-text-on-surface from global; } - layout-grid-cell { + layout-grid-cell, message-box { composes: theme-border-color-background from global; } `, css` + message-box { + padding: 16px 24px; + border-bottom: solid 1px; + } + layout-grid { width: 100%; flex: 1; @@ -59,6 +65,11 @@ const styles = composes( padding: 0 12px; padding-right: 24px; } + + p { + line-height: 2; + white-space: pre; + } ` ); @@ -73,6 +84,11 @@ export const ConnectionsAdministration = observer(function ConnectionsAdministra + {configurationWizard && ( + +

+
+ )} diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts index d9e03c5135..3f5e005db9 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationController.ts @@ -11,8 +11,7 @@ import { observable, computed } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; import { ConnectionsResource, isSearchedConnection } from '../ConnectionsResource'; import { ConnectionsAdministrationNavService } from './ConnectionsAdministrationNavService'; @@ -22,8 +21,6 @@ export class ConnectionsAdministrationController { @observable isProcessing = false; readonly selectedItems = observable(new Map()) readonly expandedItems = observable(new Map()) - readonly error = new GQLErrorCatcher(); - @computed get connections() { return Array.from(this.connectionsResource.data.values()) @@ -65,9 +62,7 @@ export class ConnectionsAdministrationController { try { await this.connectionsResource.refresh('all'); } catch (exception) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'Connections update failed'); - } + this.notificationService.logException(exception, 'Connections update failed'); } } @@ -108,17 +103,9 @@ export class ConnectionsAdministrationController { this.expandedItems.delete(id); } } catch (exception) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'Connections delete failed'); - } + this.notificationService.logException(exception, 'Connections delete failed'); } finally { this.isProcessing = false; } } - - showDetails = () => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - } } diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts index 2cdf222e7d..1f379929a1 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsAdministrationService.ts @@ -27,6 +27,7 @@ export class ConnectionsAdministrationService extends Bootstrap { } register() { + this.administrationItemService.create({ name: 'connections', type: AdministrationItemType.Default, @@ -36,10 +37,7 @@ export class ConnectionsAdministrationService extends Bootstrap { description: 'connections_administration_configuration_wizard_step_description', }, sub: [ - { - name: 'create', - getComponent: () => ConnectionsAdministration, - }, + { name: 'create' }, ], getContentComponent: () => ConnectionsAdministration, getDrawerComponent: () => ConnectionsDrawerItem, diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx index 3c83bf1a29..2f946cd0a4 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionEdit.tsx @@ -26,18 +26,13 @@ const styles = composes( `, css` box { + box-sizing: border-box; padding: 24px; min-height: 320px; max-height: 500px; display: flex; flex-direction: column; } - - ErrorMessage { - position: sticky; - bottom: 0; - padding: 8px 24px; - } ` ); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx index 927d0ea02a..b857fdde6f 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionsTable.tsx @@ -25,6 +25,9 @@ const styles = composes( } `, css` + Table { + width: 100%; + } TableColumnHeader { border-top: solid 1px; } diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx index fc5ec32189..0470ff1752 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnection.tsx @@ -202,10 +202,10 @@ export const CreateConnection = observer(function CreateConnection({ - {translate('Driver')} + {translate('connections_connection_driver')} - {translate('Search Database')} + {translate('connections_connection_create_search_database')} diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx index 0675851835..40be90b9f0 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/CreateConnectionController.tsx @@ -9,15 +9,12 @@ import { observable, computed } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher, AdminConnectionSearchInfo, ConnectionInfo } from '@cloudbeaver/core-sdk'; +import { AdminConnectionSearchInfo, ConnectionInfo } from '@cloudbeaver/core-sdk'; import { uuid } from '@cloudbeaver/core-utils'; import { DBDriverResource } from '../../../DBDriverResource'; import { ConnectionsResource } from '../../ConnectionsResource'; -import { ConnectionsAdministrationNavService } from '../ConnectionsAdministrationNavService'; @injectable() export class CreateConnectionController { @@ -37,14 +34,10 @@ export class CreateConnectionController { return this.dbDriverResource.get(this.connection.driverId); } - readonly error = new GQLErrorCatcher(); - constructor( private notificationService: NotificationService, private connectionsResource: ConnectionsResource, - private commonDialogService: CommonDialogService, private dbDriverResource: DBDriverResource, - private connectionsAdministrationNavService: ConnectionsAdministrationNavService ) { this.credentials = {}; this.databases = []; @@ -67,9 +60,7 @@ export class CreateConnectionController { this.databases = await this.connectionsResource.searchDatabases(hosts); } catch (exception) { - if (!this.error.catch(exception)) { - this.notificationService.logException(exception, 'Databases search failed'); - } + this.notificationService.logException(exception, 'Databases search failed'); } finally { this.isProcessing = false; } @@ -109,10 +100,4 @@ export class CreateConnectionController { this.connection = null; this.availableDrivers = []; } - - showDetails = () => { - if (this.error.exception) { - this.commonDialogService.open(ErrorDetailsDialog, this.error.exception); - } - } } diff --git a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx index fb9b36aca5..1dfad0a024 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/CreateConnection/DriverList.tsx @@ -10,6 +10,7 @@ import { observer } from 'mobx-react'; import { useState, useMemo } from 'react'; import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; import { DBDriver } from '../../../DBDriverResource'; import { Driver } from './Driver'; @@ -21,6 +22,7 @@ type Props = { } export const DriverList = observer(function DriverList({ drivers, className, onSelect }: Props) { + const translate = useTranslate(); const [search, setSearch] = useState(''); const filteredDrivers = useMemo(() => { if (!search) { @@ -31,7 +33,7 @@ export const DriverList = observer(function DriverList({ drivers, className, onS return ( - + {filteredDrivers.map(driver => )} ); diff --git a/webapp/packages/core-connections/src/Administration/Connections/DatabasesSearch.tsx b/webapp/packages/core-connections/src/Administration/Connections/DatabasesSearch.tsx deleted file mode 100644 index d7b2791e8e..0000000000 --- a/webapp/packages/core-connections/src/Administration/Connections/DatabasesSearch.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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'; -import styled, { css } from 'reshadow'; - -import { Button, InputField, SubmittingForm } from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; -import { useStyles, composes } from '@cloudbeaver/core-theming'; - -const styles = composes( - css` - SubmittingForm { - composes: theme-border-color-background from global; - } - `, - css` - SubmittingForm { - flex: 1; - display: flex; - flex-wrap: wrap; - align-items: center; - border-top: solid 1px; - } - - group { - box-sizing: border-box; - display: flex; - } - - action { - padding-left: 24px; - } - - InputField { - width: 450px; - } - ` -); - -type Props = { - hosts: string; - className?: string; - onChange(hosts: string): void; - onSearch(): void; - disabled?: boolean; -} - -export const DatabasesSearch = observer(function DatabasesSearch({ - hosts, - className, - onChange, - onSearch, - disabled, -}: Props) { - const translate = useTranslate(); - - return styled(useStyles(styles))( - - - - {translate('connections_connection_edit_search_hosts')} - - - - - - - ); -}); diff --git a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts index c1ff34be44..44b9e16631 100644 --- a/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts +++ b/webapp/packages/core-connections/src/Administration/ConnectionsResource.ts @@ -87,6 +87,7 @@ export class ConnectionsResource extends CachedMapResource this.updateConnection(id, config)); + await this.graphQLService.gql.refreshSessionConnections(); return this.get(id)!; } async delete(key: ResourceKey) { await this.performUpdate(key, () => this.deleteConnectionTask(key)); + await this.graphQLService.gql.refreshSessionConnections(); } async loadAccessSubjects(connectionId: string): Promise { diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 7994b1d86d..06f8e64965 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -11,15 +11,26 @@ import { ConnectionInfo, GraphQLService, CachedMapResource, - ObjectPropertyInfo + ObjectPropertyInfo, + ResourceKey, + isResourceKeyList, + resourceKeyList } from '@cloudbeaver/core-sdk'; +import { ConnectionsResource } from './Administration/ConnectionsResource'; + export type Connection = Pick & { authProperties?: ObjectPropertyInfo[] } @injectable() export class ConnectionInfoResource extends CachedMapResource { - constructor(private graphQLService: GraphQLService) { + constructor( + private graphQLService: GraphQLService, + private connectionsResource: ConnectionsResource + ) { super(new Map()); + connectionsResource.onItemAdd.subscribe(this.addHandler.bind(this)); + connectionsResource.onItemDelete.subscribe(this.delete.bind(this)); + connectionsResource.onDataOutdated.subscribe(this.markOutdated.bind(this)); } async createFromTemplate(templateId: string): Promise { @@ -79,6 +90,19 @@ export class ConnectionInfoResource extends CachedMapResource) { + if (isResourceKeyList(key)) { + this.load(resourceKeyList(key.list.filter(id => !this.connectionsResource.get(id)?.template))); + return; + } + + if (this.connectionsResource.get(key)?.template) { + return; + } + + this.load(key); + } + private async getAuthProperties(id: string): Promise { const { connection: { authProperties } } = await this.graphQLService.gql.connectionAuthProperties({ id }); diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index 62b1f171a5..108fa27a00 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -5,6 +5,8 @@ export default [ ['connections_administration_new_connection', 'New connection'], ['connections_administration_configuration_wizard_step_title', 'Database connections'], ['connections_administration_configuration_wizard_step_description', 'Database connections'], + ['connections_administration_configuration_wizard_message', 'Here you can create the database connections manually or using a database server search feature.\nYou can skip this step and configure connections later.'], + ['connections_connection_create_search_database', 'Search Database'], ['connections_connection_edit_authentication', 'Authentication'], ['connections_connection_edit_access', 'Access'], ['connections_connection_edit_access_load_failed', 'Connection access loading failed'], @@ -19,4 +21,5 @@ export default [ ['connections_connection_port', 'Port'], ['connections_connection_template', 'Template'], ['connections_connection_test', 'Test Connection'], + ['connections_driver_search_placeholder', 'Type driver name...'], ]; diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index e479556b49..dcf5c3b659 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -5,6 +5,7 @@ export default [ ['connections_administration_new_connection', 'Создание подключения'], ['connections_administration_configuration_wizard_step_title', 'Подключения к базам'], ['connections_administration_configuration_wizard_step_description', 'Подключения к базам'], + ['connections_connection_create_search_database', 'Поиск баз'], ['connections_connection_edit_authentication', 'Авторизация'], ['connections_connection_edit_access', 'Доступ'], ['connections_connection_edit_access_load_failed', 'Не удалось загрузить информацию доступа'], @@ -14,9 +15,10 @@ export default [ ['connections_connection_address', 'Адрес'], ['connections_connection_name', 'Название'], ['connections_connection_description', 'Описапние'], - ['connections_connection_driver', 'Driver'], + ['connections_connection_driver', 'Драйвер'], ['connections_connection_host', 'Хост'], ['connections_connection_port', 'Порт'], ['connections_connection_template', 'Шаблон'], ['connections_connection_test', 'Проверить подключение'], + ['connections_driver_search_placeholder', 'Введите название драйвера...'], ]; diff --git a/webapp/packages/core-localization/src/LocalizationService.ts b/webapp/packages/core-localization/src/LocalizationService.ts index 9f27570e09..2227519dcd 100644 --- a/webapp/packages/core-localization/src/LocalizationService.ts +++ b/webapp/packages/core-localization/src/LocalizationService.ts @@ -51,14 +51,16 @@ export class LocalizationService extends Bootstrap { } readonly translate = (token: TLocalizationToken): string => { - let locale = this.localeMap.get(this.getCurrentLanguage()); + let translation = this.localeMap + .get(this.getCurrentLanguage()) + ?.get(token); - if (!locale) { - locale = this.localeMap.get(DEFAULT_LOCALE_NAME); + if (!translation) { + translation = this.localeMap + .get(DEFAULT_LOCALE_NAME) + ?.get(token); } - const translation = locale && locale.get(token); - if (typeof translation === 'string') { return translation; } diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 2e4eb06d5d..5749d96f65 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -13,16 +13,16 @@ export const defaultRULocale = [ ['ui_errors_details', 'Информация'], ['ui_search', 'Поиск...'], ['ui_data_saving_error', 'Ошибка при сохранении изменений'], - ['block_properties_name', 'Название'], - ['block_properties_value', 'Значение'], - ['block_properties_add', 'Добавить'], + ['block_properties_table_name', 'Название'], + ['block_properties_table_value', 'Значение'], + ['block_properties_table_add', 'Добавить'], ['root_permission_denied', 'Отказано в доступе'], ['app_root_session_expired_title', 'Сесстия истекла'], ['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'], ['app_root_session_expired_reload', 'Перезагрузить'], ['app_shared_connectionMenu_newConnection', 'Новое подключение'], ['app_shared_connectionMenu_connection', 'Подключение'], - ['app_shared_connectionMenu_custom', 'Custom'], + ['app_shared_connectionMenu_custom', 'Настроить'], ['app_shared_connectionMenu_disconnect', 'Отключить'], ['app_shared_toolsMenu_tools', 'Инструменты'], ['app_shared_toolsMenu_logViewer', 'Просмотр лога'], diff --git a/webapp/packages/core-sdk/src/CachedMapResource.ts b/webapp/packages/core-sdk/src/CachedMapResource.ts index d1ec20bf5e..6730a491b8 100644 --- a/webapp/packages/core-sdk/src/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/CachedMapResource.ts @@ -48,7 +48,13 @@ export abstract class CachedMapResource extends CachedResource< return this.outdated.has(key); } - markOutdated(key: ResourceKey): void { + markOutdated(): void + markOutdated(key: ResourceKey): void + markOutdated(key?: ResourceKey): void { + if (!key) { + key = resourceKeyList(Array.from(this.data.keys())); + } + if (isResourceKeyList(key)) { for (const itemKey of key.list) { this.outdated.add(itemKey); @@ -59,7 +65,13 @@ export abstract class CachedMapResource extends CachedResource< this.outdatedSubject.next(key); } - markUpdated(key: ResourceKey): void { + markUpdated(): void + markUpdated(key: ResourceKey): void + markUpdated(key?: ResourceKey): void { + if (!key) { + key = resourceKeyList(Array.from(this.data.keys())); + } + if (isResourceKeyList(key)) { for (const itemKey of key.list) { this.outdated.delete(itemKey); diff --git a/webapp/packages/core-sdk/src/queries/connections/refreshSessionConnections.gql b/webapp/packages/core-sdk/src/queries/connections/refreshSessionConnections.gql new file mode 100644 index 0000000000..0113443602 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/connections/refreshSessionConnections.gql @@ -0,0 +1,3 @@ +mutation refreshSessionConnections { + refreshSessionConnections +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index cf26f636c1..dd0ed7e8a5 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -252,6 +252,7 @@ export type Mutation = { openConnection: ConnectionInfo; openSession: SessionInfo; readDataFromContainer?: Maybe; + refreshSessionConnections?: Maybe; setConnectionNavigatorSettings: Scalars['Boolean']; sqlContextCreate: SqlContextInfo; sqlContextDestroy: Scalars['Boolean']; @@ -1023,6 +1024,10 @@ export type InitConnectionMutationVariables = Exact<{ export type InitConnectionMutation = { connection: Pick }; +export type RefreshSessionConnectionsMutationVariables = Exact<{ [key: string]: never }>; + +export type RefreshSessionConnectionsMutation = Pick; + export type GetTemplateConnectionsQueryVariables = Exact<{ [key: string]: never }>; export type GetTemplateConnectionsQuery = { connections: Array> }; @@ -1712,6 +1717,11 @@ export const InitConnectionDocument = ` } } `; +export const RefreshSessionConnectionsDocument = ` + mutation refreshSessionConnections { + refreshSessionConnections +} + `; export const GetTemplateConnectionsDocument = ` query getTemplateConnections { connections: templateConnections { @@ -2295,6 +2305,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = initConnection(variables: InitConnectionMutationVariables): Promise { return withWrapper(() => client.request(InitConnectionDocument, variables)); }, + refreshSessionConnections(variables?: RefreshSessionConnectionsMutationVariables): Promise { + return withWrapper(() => client.request(RefreshSessionConnectionsDocument, variables)); + }, getTemplateConnections(variables?: GetTemplateConnectionsQueryVariables): Promise { return withWrapper(() => client.request(GetTemplateConnectionsDocument, variables)); }, diff --git a/webapp/packages/core-theming/src/styles/_table.scss b/webapp/packages/core-theming/src/styles/_table.scss index c4edd9db1c..6ee29790cf 100644 --- a/webapp/packages/core-theming/src/styles/_table.scss +++ b/webapp/packages/core-theming/src/styles/_table.scss @@ -12,10 +12,15 @@ min-width: 100%; text-align: left; border-collapse: collapse; + table-layout: fixed; &[use|size="big"] { > thead > tr > th { height: 54px; + + &[use|min] { + width: 46px; + } } > tbody > tr > td { @@ -53,6 +58,7 @@ } th { + box-sizing: border-box; white-space: nowrap; padding: 16px; height: 36px; @@ -64,7 +70,7 @@ text-decoration: none !important; &[use|min] { - width: 1%; + width: 28px; } &:last-child { @@ -74,6 +80,7 @@ td { position: relative; + box-sizing: border-box; height: 28px; padding: 0 16px; transition: padding ease-in-out 0.24s; diff --git a/webapp/packages/core-theming/src/styles/_tabs.scss b/webapp/packages/core-theming/src/styles/_tabs.scss index 225d0346f6..e18f471fad 100644 --- a/webapp/packages/core-theming/src/styles/_tabs.scss +++ b/webapp/packages/core-theming/src/styles/_tabs.scss @@ -84,6 +84,8 @@ } tab-title { + overflow: hidden; + text-overflow: ellipsis; padding: 0 12px; } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser.tsx index 0ff36b77d6..9b00d72650 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/CreateUser.tsx @@ -28,13 +28,13 @@ const styles = composes( css` title-bar { composes: theme-typography--headline6 from global; - padding: 0 16px; + box-sizing: border-box; + padding: 16px 24px; border-top: solid 1px; align-items: center; - height: 48px; display: flex; font-weight: 400; - flex: 1; + flex: auto 0 0; } ` ); @@ -47,7 +47,7 @@ export function CreateUser({ return styled(useStyles(styles))( - + ); } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx index 9c5861ee45..210f58da14 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx @@ -21,6 +21,7 @@ import { } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; +import { AdminUserInfo } from '@cloudbeaver/core-sdk'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import { GrantedConnections } from './GrantedConnections'; @@ -53,10 +54,6 @@ const styles = composes( } `, css` - box { - padding: 24px; - } - SubmittingForm { flex: 1; display: flex; @@ -128,16 +125,18 @@ const styles = composes( ); type Props = { - userId: string; + user: AdminUserInfo; + editing?: boolean; onCancel: () => void; } export const UserForm = observer(function UserForm({ - userId, + user, + editing = false, onCancel, }: Props) { const translate = useTranslate(); - const controller = useController(UserFormController, userId, onCancel); + const controller = useController(UserFormController, user, editing, onCancel); const [focusedRef] = useFocus({ focusFirstChild: true }); const handleLoginChange = useCallback( @@ -182,7 +181,7 @@ export const UserForm = observer(function UserForm({ mod={['unelevated']} onClick={controller.save} > - {translate(controller.isNew ? 'ui_processing_create' : 'ui_processing_save')} + {translate(!editing ? 'ui_processing_create' : 'ui_processing_save')} @@ -199,7 +198,7 @@ export const UserForm = observer(function UserForm({ name='login' value={controller.credentials.login} onChange={handleLoginChange} - disabled={!controller.isNew || controller.isSaving} + disabled={editing || controller.isSaving} mod='surface' required > diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts index feba6eb8b2..6edbc7a5f9 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormController.ts @@ -31,10 +31,6 @@ export class UserFormController implements IInitializableController, IDestructib roles: new Map(), }; - @computed get isNew() { - return this.usersResource.isNew(this.userId); - } - @computed get connections() { return Array.from(this.connectionsResource.data.values()) .filter(connection => !this.connectionsResource.isNew(connection.id)); @@ -44,16 +40,14 @@ export class UserFormController implements IInitializableController, IDestructib return Array.from(this.rolesManagerService.roles.data.values()); } - @computed get user(): AdminUserInfo { - return this.usersResource.get(this.userId)!; - } + user!: AdminUserInfo; readonly error = new GQLErrorCatcher(); private isDistructed = false; - private userId!: string; private connectionAccessChanged = false; private connectionAccessLoaded = false; private collapse!: () => void; + private editing!: boolean; constructor( private notificationService: NotificationService, @@ -64,8 +58,9 @@ export class UserFormController implements IInitializableController, IDestructib private dbDriverResource: DBDriverResource ) { } - init(id: string, collapse: () => void) { - this.userId = id; + init(user: AdminUserInfo, editing: boolean, collapse: () => void) { + this.user = user; + this.editing = editing; this.collapse = collapse; this.loadRoles(); } @@ -84,10 +79,9 @@ export class UserFormController implements IInitializableController, IDestructib this.isSaving = true; try { - if (this.isNew) { + if (!this.editing) { await this.usersResource.create({ userId: this.credentials.login, - newId: this.userId, credentials: { password: this.credentials.password }, roles: this.getGrantedRoles(), grantedConnections: this.getGrantedConnections(), @@ -103,12 +97,9 @@ export class UserFormController implements IInitializableController, IDestructib await this.usersResource.refresh(this.user.userId); this.notificationService.logInfo({ title: 'authentication_administration_user_updated' }); } - - this.connectionAccessLoaded = false; - await this.loadConnectionsAccess(); } catch (exception) { if (!this.error.catch(exception) || this.isDistructed) { - if (this.isNew) { + if (!this.editing) { this.notificationService.logException(exception, 'Error creating new user'); } else { this.notificationService.logException(exception, 'Error saving user'); @@ -134,12 +125,14 @@ export class UserFormController implements IInitializableController, IDestructib this.isLoading = true; try { - this.grantedConnections = await this.usersResource.loadConnections(this.userId); + if (this.editing) { + this.grantedConnections = await this.usersResource.loadConnections(this.user.userId); - this.selectedConnections.clear(); - for (const connection of this.grantedConnections) { - if (connection.subjectType !== AdminSubjectType.Role) { - this.selectedConnections.set(connection.connectionId, true); + this.selectedConnections.clear(); + for (const connection of this.grantedConnections) { + if (connection.subjectType !== AdminSubjectType.Role) { + this.selectedConnections.set(connection.connectionId, true); + } } } this.connectionAccessLoaded = true; @@ -161,12 +154,12 @@ export class UserFormController implements IInitializableController, IDestructib return; } - if (!this.credentials.password && this.isNew) { + if (!this.credentials.password && !this.editing) { this.notificationService.logError({ title: 'authentication_user_password_not_set' }); return; } - if (!this.credentials.password && this.isNew) { + if (!this.credentials.password && !this.editing) { this.notificationService.logError({ title: 'authentication_user_password_not_set' }); return; } @@ -190,7 +183,7 @@ export class UserFormController implements IInitializableController, IDestructib if (!this.user.grantedRoles.includes(roleId)) { await this.usersResource.grantRole(this.user.userId, roleId, true); } - } else if (!this.isNew) { + } else { await this.usersResource.revokeRole(this.user.userId, roleId, true); } } @@ -215,8 +208,10 @@ export class UserFormController implements IInitializableController, IDestructib if (!this.connectionAccessChanged) { return; } - await this.usersResource.setConnections(this.userId, this.getGrantedConnections()); + await this.usersResource.setConnections(this.user.userId, this.getGrantedConnections()); this.connectionAccessChanged = false; + this.connectionAccessLoaded = false; + await this.loadConnectionsAccess(); } private async loadRoles() { @@ -232,11 +227,7 @@ export class UserFormController implements IInitializableController, IDestructib private async loadUser() { try { - await this.usersResource.load(this.userId); - - if (!this.isNew) { - this.credentials.login = this.user.userId; - } + this.credentials.login = this.user.userId; this.credentials.roles = new Map(this.user.grantedRoles.map(roleId => ([roleId, true]))); } catch (exception) { this.notificationService.logException(exception, 'Can\'t load user'); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts index 8df7843c18..351e00765a 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts @@ -13,7 +13,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; -import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk'; +import { GQLErrorCatcher, resourceKeyList, AdminUserInfo } from '@cloudbeaver/core-sdk'; import { UsersAdministrationNavigationService } from './UsersAdministrationNavigationService'; @@ -26,10 +26,9 @@ export class UsersAdministrationController { @computed get users() { return Array.from(this.usersResource.data.values()) - .filter(user => !this.usersResource.isNew(user.userId)) .sort((a, b) => { if (this.usersResource.isNew(a.userId) === this.usersResource.isNew(b.userId)) { - return 0; + return a.userId.localeCompare(b.userId); } if (this.usersResource.isNew(a.userId)) { return -1; @@ -38,10 +37,7 @@ export class UsersAdministrationController { }); } - @computed get creatingUser() { - return Array.from(this.usersResource.data.values()) - .find(user => this.usersResource.isNew(user.userId)); - } + @observable creatingUser: AdminUserInfo | null = null; get isLoading() { return this.usersResource.isLoading() || this.isDeleting; @@ -55,7 +51,13 @@ export class UsersAdministrationController { ) { } create = () => { - this.usersResource.addNew(); + this.creatingUser = { + userId: '', + grantedRoles: [], + grantedConnections: [], + configurationParameters: {}, + metaParameters: {}, + } as AdminUserInfo; this.usersAdministrationNavigationService.navToAdd(); } @@ -64,7 +66,7 @@ export class UsersAdministrationController { return; } - this.usersResource.delete(this.creatingUser.userId); + this.creatingUser = null; this.usersAdministrationNavigationService.navToRoot(); } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts index de2cec3da0..9078aaa62c 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts @@ -32,7 +32,6 @@ export class UsersAdministrationService extends Bootstrap { sub: [ { name: UsersAdministrationNavigationService.AddItemName, - getComponent: () => UsersAdministration, onActivate: this.onCreateActivate.bind(this), }, ], diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx index 760f123893..2f0a814f10 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx @@ -7,14 +7,11 @@ */ import { observer } from 'mobx-react'; -import styled, { css, use } from 'reshadow'; +import styled, { css } from 'reshadow'; -import { UsersResource } from '@cloudbeaver/core-authentication'; import { TableItem, TableColumnValue, TableItemSelect, TableItemExpand } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { useTranslate } from '@cloudbeaver/core-localization'; import { AdminUserInfo } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -31,9 +28,6 @@ type Props = { } export const User = observer(function User({ user }: Props) { - const translate = useTranslate(); - const usersResource = useService(UsersResource); - const isNew = usersResource.isNew(user.userId); return styled(useStyles(styles))( @@ -43,9 +37,9 @@ export const User = observer(function User({ user }: Props) { - {isNew ? translate('authentication_administration_user_connections_user_new') : user.userId} + {user.userId} {user.grantedRoles.join(', ')} - {isNew && {translate('ui_tag_new')}} + ); }); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx index ab38706268..3146c39f2c 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEdit.tsx @@ -8,12 +8,34 @@ import { observer } from 'mobx-react'; import { useContext, useCallback } from 'react'; +import styled, { css } from 'reshadow'; import { TableContext, } from '@cloudbeaver/core-blocks'; +import { useController } from '@cloudbeaver/core-di'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; import { UserForm } from '../UserForm/UserForm'; +import { UserEditController } from './UserEditController'; + +const styles = composes( + css` + box { + composes: theme-background-secondary theme-text-on-secondary from global; + } + `, + css` + box { + box-sizing: border-box; + padding: 24px; + min-height: 320px; + max-height: 500px; + display: flex; + flex-direction: column; + } + ` +); type Props = { item: string; @@ -22,8 +44,15 @@ type Props = { export const UserEdit = observer(function UserEdit({ item, }: Props) { + const controller = useController(UserEditController, item); const tableContext = useContext(TableContext); const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]); - return ; + return styled(useStyles(styles))( + + {controller.user && ( + + )} + + ); }); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEditController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEditController.ts new file mode 100644 index 0000000000..6210dbd87c --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserEditController.ts @@ -0,0 +1,55 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 } from 'mobx'; + +import { UsersResource } from '@cloudbeaver/core-authentication'; +import { + injectable, IInitializableController, IDestructibleController +} from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { GQLErrorCatcher, AdminUserInfo } from '@cloudbeaver/core-sdk'; + +@injectable() +export class UserEditController +implements IInitializableController, IDestructibleController { + @observable isLoading = true; + @observable user: AdminUserInfo | null = null; + + @computed get isDisabled() { + return this.isLoading; + } + + userId!: string; + + readonly error = new GQLErrorCatcher(); + + constructor( + private notificationService: NotificationService, + private usersResource: UsersResource, + ) { } + + init(id: string) { + this.userId = id; + this.loadUser(); + } + + destruct(): void { } + + private async loadUser() { + this.isLoading = true; + try { + // we create a copy to protect the current value from mutation + this.user = JSON.parse(JSON.stringify(await this.usersResource.load(this.userId))); + } catch (exception) { + this.notificationService.logException(exception, `Can't load user ${this.userId}`); + } finally { + this.isLoading = false; + } + } +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx index 1cd3d78b9f..727e2b2972 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UsersTable.tsx @@ -19,6 +19,9 @@ import { useStyles } from '@cloudbeaver/core-theming'; import { User } from './User'; const styles = css` + Table { + width: 100%; + } TableColumnHeader { border-top: solid 1px; } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 8564fcd4b3..f476a1f5e4 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -35,7 +35,7 @@ export class AuthenticationService extends Bootstrap { throw new Error('Can\'t configure Authentication'); } - if (!config.authenticationEnabled) { + if (!config.authenticationEnabled || config.configurationMode) { return; } diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionFormDialogController.ts b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionFormDialogController.ts index 33af008bcd..33aa7dbe83 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionFormDialogController.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/ConnectionFormDialogController.ts @@ -136,12 +136,12 @@ implements IInitializableController, IDestructibleController { @action private setDriverDefaults() { - this.config.name = `${this.driver?.name} (custom)`; - this.config.driverId = this.driver?.id; - this.config.host = ''; - this.config.port = this.driver?.defaultPort || ''; - this.config.databaseName = ''; - this.config.url = this.driver?.sampleURL || ''; + this.config.name = `${this.driver.name} (custom)`; + this.config.driverId = this.driver.id; + this.config.host = this.driver.defaultServer || 'localhost'; + this.config.port = this.driver.defaultPort || ''; + this.config.databaseName = this.driver.defaultDatabase; + this.config.url = this.driver.sampleURL || ''; this.config.properties = {}; this.config.authModelId = this.driver.defaultAuthModel; this.config.credentials = {}; diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverProperties.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverProperties.tsx index e5b4b2714a..98978f1a40 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverProperties.tsx +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverProperties.tsx @@ -41,7 +41,7 @@ export const DriverProperties = observer(function DriverProperties({ state, loadProperties, }: DriverPropertiesProps) { - const controller = useController(DriverPropertiesController, driver); + const controller = useController(DriverPropertiesController, driver, state); useEffect(() => { if (loadProperties) { @@ -56,7 +56,7 @@ export const DriverProperties = observer(function DriverProperties({ )} diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts index 9a1d574942..85a3ad9fc8 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/ConnectionFormDialog/DriverProperties/DriverPropertiesController.ts @@ -34,21 +34,24 @@ export class DriverPropertiesController implements IInitializableController { @observable driverProperties = observable([]) private loaded = false; + private state!: Record; constructor( private driverPropertiesService: DriverPropertiesService, private notificationService: NotificationService ) { } - init(driver: DBDriver) { + init(driver: DBDriver, state: Record) { this.driver = driver; + this.state = state; } - onAddProperty = () => { + addProperty = (key?: string, value?: string) => { this.driverProperties.unshift({ id: uuid(), - key: 'property', - defaultValue: '', + key: key ?? 'property', + defaultValue: value ?? '', + new: !key, }); } @@ -67,6 +70,14 @@ export class DriverPropertiesController implements IInitializableController { description: property.description, validValues: property.validValues, }))); + + for (const key of Object.keys(this.state)) { + if (this.driverProperties.some(property => property.key === key)) { + continue; + } + + this.addProperty(key, this.state[key]); + } this.loaded = true; } catch (exception) { this.notificationService.logException(exception, 'Can\'t load driver properties'); diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx index e0a21f4a0d..ef5938490c 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx +++ b/webapp/packages/plugin-connection-custom/src/CustomConnection/DriverSelectorDialog/DriverSelector.tsx @@ -10,6 +10,7 @@ import { observer } from 'mobx-react'; import { useState, useMemo } from 'react'; import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; import { Driver, IDriver } from './Driver'; @@ -20,6 +21,7 @@ type DriverSelectorProps = { } export const DriverSelector = observer(function DriverSelector({ drivers, className, onSelect }: DriverSelectorProps) { + const translate = useTranslate(); const [search, setSearch] = useState(''); const filteredDrivers = useMemo(() => { if (!search) { @@ -30,7 +32,7 @@ export const DriverSelector = observer(function DriverSelector({ drivers, classN return ( - + {filteredDrivers.map(driver => )} ); diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts index 744166d363..af98a39f38 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/AgGridTableController.ts @@ -18,7 +18,6 @@ import { ColDef, ValueGetterParams, GridOptions, - CellEditingStoppedEvent, CellClassParams, SortChangedEvent, RowNode, @@ -74,7 +73,6 @@ export class AgGridTableController implements IInitializableController, IDestruc onGridReady: this.handleGridReady.bind(this), onBodyScroll: this.handleBodyScroll.bind(this), - onCellEditingStopped: this.handleCellEditingStopped.bind(this), onSortChanged: this.handleSortChanged.bind(this), }; @@ -163,11 +161,6 @@ export class AgGridTableController implements IInitializableController, IDestruc this.gridModel.onCellEditingStopped(rowIndex, colId, value, editing); } - private handleCellEditingStopped(event: CellEditingStoppedEvent) { - // TODO: probably no more needed because editCellValue executes on every change in editor - // this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getColId(), event.value, false); - } - private isCellEdited(rowIndex: number, column: string) { return this.gridModel.isCellEdited(rowIndex, column); } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index f803a941bb..e300f22a84 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -75,7 +75,16 @@ export const TableFooter = observer(function TableFooter({ }: TableFooterProps) { const ref = useRef(null); const handleChange = useCallback( - () => ref.current && model.setChunkSize(parseInt(ref.current.value, 10)), + () => { + if (!ref.current) { + return; + } + const value = parseInt(ref.current.value, 10); + + if (model.getChunkSize() !== value) { + model.setChunkSize(value); + } + }, [model] );