diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 5425bb9bca..04f0bc0b15 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -90,6 +90,8 @@ type ServerConfig { version: String! workspaceId: ID! + serverURL: String! + anonymousAccessEnabled: Boolean authenticationEnabled: Boolean @deprecated supportsCustomConnections: Boolean diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 790196f905..b5f4388af9 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -28,7 +28,7 @@ type SQLCompletionProposal { replacementString: String! replacementOffset: Int! replacementLength: Int! - cursorPosition: Int! + cursorPosition: Int icon: String nodePath: String diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java index 286726eb75..82b83e7725 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java @@ -45,6 +45,17 @@ public interface DBWAuthProviderExternal extend @NotNull Map authParameters // Passed auth parameters (e.g. user name or password) ) throws DBException; + /** + * Returns new identifying credentials which can be used to find/create user in database + */ + @NotNull + Map authFederatedUser( + @NotNull DBRProgressMonitor monitor, + @NotNull Map providerConfig, // Auth provider configuration (e.g. 3rd party auth server address) + @NotNull String faProvider, + @NotNull Map faParameters // Passed federated auth parameters (e.g. assertion ID, session token, etc) + ) throws DBException; + /** * Validates that external user may be associated with local user * @param userCredentials credentials from authExternalUser diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java index 606357882e..a4f089836a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/AbstractExternalAuthProvider.java @@ -18,9 +18,14 @@ package io.cloudbeaver.auth.provider; import io.cloudbeaver.DBWAuthProviderExternal; import io.cloudbeaver.model.user.WebUser; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.access.DBASession; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.utils.CommonUtils; +import java.util.Map; + /** * Abstract external auth provider */ @@ -34,4 +39,9 @@ public abstract class AbstractExternalAuthProvider i } } + @NotNull + @Override + public Map authFederatedUser(@NotNull DBRProgressMonitor monitor, @NotNull Map providerConfig, @NotNull String faProvider, @NotNull Map faParameters) throws DBException { + throw new DBException("Federated authorization not supported"); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index b65aae1202..611b731d85 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -59,6 +59,11 @@ public class WebServerConfig { return DBWorkbench.getPlatform().getWorkspace().getWorkspaceId(); } + @Property + public String getServerURL() { + return CommonUtils.notEmpty(application.getServerURL()); + } + @Property public boolean isAnonymousAccessEnabled() { return application.getAppConfiguration().isAnonymousAccessEnabled(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java index 649850ced5..2d972545c2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java @@ -19,6 +19,7 @@ package io.cloudbeaver.server; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebServiceRegistry; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings; @@ -46,6 +47,7 @@ public class CBAppConfig { private String[] enabledAuthProviders = null; private DataSourceNavigatorSettings defaultNavigatorSettings = DEFAULT_VIEW_SETTINGS; private Map plugins = new LinkedHashMap<>(); + private Map authConfiguration = new LinkedHashMap<>(); public boolean isAnonymousAccessEnabled() { return anonymousAccessEnabled; @@ -132,15 +134,16 @@ public class CBAppConfig { this.defaultNavigatorSettings = new DataSourceNavigatorSettings(defaultNavigatorSettings); } + @NotNull public Map getPlugins() { return plugins; } - public Map getPluginConfig(String pluginId) { + public Map getPluginConfig(@NotNull String pluginId) { return getPluginConfig(pluginId, false); } - public Map getPluginConfig(String pluginId, boolean create) { + public Map getPluginConfig(@NotNull String pluginId, boolean create) { Object config = plugins.get(pluginId); if (config instanceof Map) { return (Map) config; @@ -155,8 +158,21 @@ public class CBAppConfig { } } - public T getPluginOption(String pluginId, String option) { + public T getPluginOption(@NotNull String pluginId, @NotNull String option) { return (T)getPluginConfig(pluginId).get(option); } + public Map getAuthConfiguration() { + return authConfiguration; + } + + @NotNull + public Map getAuthConfiguration(@NotNull String providerId) { + Object apConfig = authConfiguration.get(providerId); + if (apConfig instanceof Map) { + return (Map) apConfig; + } + return Collections.emptyMap(); + } + } 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 279c036d15..af94669d8a 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 @@ -74,6 +74,7 @@ public class CBApplication extends BaseApplicationImpl { return (CBApplication) BaseApplicationImpl.getInstance(); } + private String serverURL; private int serverPort = CBConstants.DEFAULT_SERVER_PORT; private String serverName = null; private String contentRoot = CBConstants.DEFAULT_CONTENT_ROOT; @@ -103,6 +104,10 @@ public class CBApplication extends BaseApplicationImpl { public CBApplication() { } + public String getServerURL() { + return serverURL; + } + public int getServerPort() { return serverPort; } @@ -403,6 +408,12 @@ public class CBApplication extends BaseApplicationImpl { Map serverConfig = JSONUtils.getObject(configProps, "server"); serverPort = JSONUtils.getInteger(serverConfig, CBConstants.PARAM_SERVER_PORT, serverPort); + if (serverConfig.containsKey(CBConstants.PARAM_SERVER_URL)) { + serverURL = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_URL, serverURL); + } else if (serverURL == null) { + serverURL = "http://" + InetAddress.getLocalHost().getHostName() + ":" + serverPort; + } + serverName = JSONUtils.getString(serverConfig, CBConstants.PARAM_SERVER_NAME, serverName); contentRoot = getRelativePath( JSONUtils.getString(serverConfig, CBConstants.PARAM_CONTENT_ROOT, contentRoot), homeFolder); @@ -522,6 +533,7 @@ public class CBApplication extends BaseApplicationImpl { public synchronized void finishConfiguration( @NotNull String newServerName, + @NotNull String newServerURL, @NotNull String adminName, @Nullable String adminPassword, @NotNull List authInfoList, @@ -538,7 +550,7 @@ public class CBApplication extends BaseApplicationImpl { // Save runtime configuration log.debug("Saving runtime configuration"); - saveRuntimeConfig(newServerName, sessionExpireTime, appConfig); + saveRuntimeConfig(newServerName, newServerURL, sessionExpireTime, appConfig); // Grant permissions to predefined connections if (isConfigurationMode() && appConfig.isAnonymousAccessEnabled()) { @@ -561,7 +573,7 @@ public class CBApplication extends BaseApplicationImpl { } public synchronized void flushConfiguration() throws DBException { - saveRuntimeConfig(serverName, maxSessionIdleTime, appConfiguration); + saveRuntimeConfig(serverName, serverURL, maxSessionIdleTime, appConfiguration); } @@ -583,7 +595,7 @@ public class CBApplication extends BaseApplicationImpl { } } - private void saveRuntimeConfig(String newServerName, long sessionExpireTime, CBAppConfig appConfig) throws DBException { + private void saveRuntimeConfig(String newServerName, String newServerURL, long sessionExpireTime, CBAppConfig appConfig) throws DBException { File runtimeConfigFile = getRuntimeAppConfigFile(); try (Writer out = new OutputStreamWriter(new FileOutputStream(runtimeConfigFile), StandardCharsets.UTF_8)) { @@ -600,6 +612,9 @@ public class CBApplication extends BaseApplicationImpl { if (!CommonUtils.isEmpty(newServerName)) { JSONUtils.field(json, CBConstants.PARAM_SERVER_NAME, newServerName); } + if (!CommonUtils.isEmpty(newServerURL)) { + JSONUtils.field(json, CBConstants.PARAM_SERVER_URL, newServerURL); + } if (sessionExpireTime > 0) { JSONUtils.field(json, CBConstants.PARAM_SESSION_EXPIRE_PERIOD, sessionExpireTime); } @@ -643,6 +658,9 @@ public class CBApplication extends BaseApplicationImpl { if (!CommonUtils.isEmpty(appConfig.getPlugins())) { JSONUtils.serializeProperties(json, "plugins", appConfig.getPlugins()); } + if (!CommonUtils.isEmpty(appConfig.getAuthConfiguration())) { + JSONUtils.serializeProperties(json, "authConfiguration", appConfig.getAuthConfiguration()); + } json.endObject(); } 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 fbae82e57c..1c2599b0aa 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 @@ -38,6 +38,7 @@ public class CBConstants { public static final String PARAM_SERVER_PORT = "serverPort"; public static final String PARAM_SERVER_NAME = "serverName"; public static final String PARAM_CONTENT_ROOT = "contentRoot"; + public static final String PARAM_SERVER_URL = "serverURL"; public static final String PARAM_ROOT_URI = "rootURI"; public static final String PARAM_SERVICES_URI = "serviceURI"; public static final String PARAM_DRIVERS_LOCATION = "driversLocation"; @@ -69,6 +70,7 @@ public class CBConstants { public static final String VAR_CB_LOCAL_HOST_ADDR = "CB_LOCAL_HOST_ADDR"; + public static final String DEFAULT_SERVER_ADDR = "localhost"; public static final String VAR_HOST_DOCKER_INTERNAL = "host.docker.internal"; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 410e0c584a..eaa1caa440 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -45,8 +45,9 @@ public class CBJettyServer { // Handler configuration ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); servletContextHandler.setResourceBase(application.getContentRoot()); - servletContextHandler.setContextPath(application.getRootURI()); - servletContextHandler.addServlet(new ServletHolder("static", new CBStaticServlet()), application.getRootURI()); + String rootURI = application.getRootURI(); + servletContextHandler.setContextPath(rootURI); + servletContextHandler.addServlet(new ServletHolder("static", new CBStaticServlet()), "/"); servletContextHandler.addServlet(new ServletHolder("images", new CBImageServlet()), application.getServicesURI() + "images/*"); servletContextHandler.addServlet(new ServletHolder("graphql", new GraphQLEndpoint()), application.getServicesURI() + "gql/*"); servletContextHandler.addEventListener(new CBServerContextListener()); diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 46b706fe95..468655d8a0 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -50,6 +50,7 @@ type AdminPermissionInfo { input ServerConfigInput { serverName: String + serverURL: String adminName: String adminPassword: String diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java index db231e48ce..8a4888727a 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java @@ -30,6 +30,7 @@ import java.util.Map; public class AdminServerConfig { private String serverName; + private String serverURL; private String adminName; private String adminPassword; @@ -44,6 +45,7 @@ public class AdminServerConfig { public AdminServerConfig(Map params) { this.serverName = JSONUtils.getString(params, "serverName"); + this.serverURL = JSONUtils.getString(params, "serverURL"); this.adminName = JSONUtils.getString(params, "adminName"); this.adminPassword = JSONUtils.getString(params, "adminPassword"); @@ -69,6 +71,14 @@ public class AdminServerConfig { this.serverName = serverName; } + public String getServerURL() { + return serverURL; + } + + public void setServerURL(String serverURL) { + this.serverURL = serverURL; + } + public String getAdminName() { return adminName; } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index f2aa4a383e..1f151c7dcf 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -419,6 +419,7 @@ public class WebServiceAdmin implements DBWServiceAdmin { CBApplication.getInstance().finishConfiguration( config.getServerName(), + config.getServerURL(), adminName, adminPassword, authInfoList, diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx index 03eb1f53ab..cea859bdf1 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx @@ -45,7 +45,7 @@ export const TreeNodeMenu = observer(function TreeNodeMenu({ } return styled(treeNodeMenuStyles)( - + ); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index f3cb2640cc..3674ce5d3e 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -8,7 +8,7 @@ import { action, makeObservable } from 'mobx'; -import { ConnectionAuthService } from '@cloudbeaver/core-connections'; +import { ConnectionAuthService, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; @@ -37,6 +37,7 @@ export class NavigationTreeService { private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, private connectionAuthService: ConnectionAuthService, + private connectionInfoResource: ConnectionInfoResource, private navNodeExtensionsService: NavNodeExtensionsService, private navTreeResource: NavTreeResource ) { @@ -61,10 +62,22 @@ export class NavigationTreeService { await this.navNodeManagerService.navToNode(id, parentId); } - async loadNestedNodes(id = ROOT_NODE_PATH): Promise { + async loadNestedNodes(id = ROOT_NODE_PATH, tryConnect?: boolean): Promise { try { - if (this.isConnectionNode(id) && !(await this.tryInitConnection(id))) { - return false; + if (this.isConnectionNode(id)) { + const connection = await this.connectionInfoResource.load( + NodeManagerUtils.connectionNodeIdToConnectionId(id) + ); + + if (!connection.connected && !tryConnect) { + return false; + } + + const connected = await this.tryInitConnection(id); + + if (!connected) { + return false; + } } await this.navNodeManagerService.loadTree(id); return true; diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts index 4556dd9717..89f1c344da 100644 --- a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -88,7 +88,7 @@ export function useElementsTree(options: IOptions): IElementsTree { continue; } - const loaded = await navTreeService.loadNestedNodes(child); + const loaded = await navTreeService.loadNestedNodes(child, false); if (!loaded) { const node = navNodeInfoResource.get(child); @@ -205,7 +205,7 @@ export function useElementsTree(options: IOptions): IElementsTree { try { if (state) { - state = await navTreeService.loadNestedNodes(node.id); + state = await navTreeService.loadNestedNodes(node.id, true); } await options.onExpand?.(node, state); diff --git a/webapp/packages/core-app/src/TopNavBar/MainMenu/MainMenu.tsx b/webapp/packages/core-app/src/TopNavBar/MainMenu/MainMenu.tsx index 45da064f64..e8ce659d61 100644 --- a/webapp/packages/core-app/src/TopNavBar/MainMenu/MainMenu.tsx +++ b/webapp/packages/core-app/src/TopNavBar/MainMenu/MainMenu.tsx @@ -31,7 +31,7 @@ export const MainMenu = observer(function MainMenu() { const mainMenuService = useService(MainMenuService); return styled(styles)( - + {mainMenuService.getMainMenu().map((topItem, i) => ( ))} diff --git a/webapp/packages/core-app/src/TopNavBar/SettingsMenu/SettingsMenu.tsx b/webapp/packages/core-app/src/TopNavBar/SettingsMenu/SettingsMenu.tsx index f13b3ba9da..ff4b750637 100644 --- a/webapp/packages/core-app/src/TopNavBar/SettingsMenu/SettingsMenu.tsx +++ b/webapp/packages/core-app/src/TopNavBar/SettingsMenu/SettingsMenu.tsx @@ -22,7 +22,7 @@ export const SettingsMenu = observer(function SettingsMenu() { const settingsMenuService = useService(SettingsMenuService); return styled(useStyles(settingsMenuStyles))( - + ); diff --git a/webapp/packages/core-app/src/TopNavBar/SettingsMenu/settingsMenuStyles.ts b/webapp/packages/core-app/src/TopNavBar/SettingsMenu/settingsMenuStyles.ts index b5bf2450d2..d986965784 100644 --- a/webapp/packages/core-app/src/TopNavBar/SettingsMenu/settingsMenuStyles.ts +++ b/webapp/packages/core-app/src/TopNavBar/SettingsMenu/settingsMenuStyles.ts @@ -26,10 +26,8 @@ export const settingsMenuStyles = css` height: 24px; } menu-panel-item { - flex-direction: row-reverse; & menu-item-text { text-align: right; - margin-left: auto; } & Icon { transform: rotate(90deg); diff --git a/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx b/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx index 7200b5d3a0..adc93ce70b 100644 --- a/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx +++ b/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx @@ -25,7 +25,8 @@ type TopMenuItemProps = Omit, 'style'> & { export const TopMenuItem = observer(function TopMenuItem({ menuItem, style = [], ...props }: TopMenuItemProps) { const translate = useTranslate(); - const styles = useStyles(!menuItem.panel ? [] : [...style, topMenuStyles]); + style = [...style, topMenuStyles]; + const styles = useStyles(!menuItem.panel ? [] : style); if (!menuItem.panel) { return ( @@ -44,12 +45,13 @@ export const TopMenuItem = observer(function TopMenuItem({ menuItem, style = [], {...props} panel={menuItem.panel} disabled={menuItem.isDisabled} - style={[...style, topMenuStyles]} + style={style} placement="bottom-end" + modal onClick={() => menuItem.onClick?.()} > {menuItem.icon && ( - + )} diff --git a/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts b/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts index 8c45278b29..d835506b17 100644 --- a/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts +++ b/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts @@ -12,7 +12,7 @@ import { composes } from '@cloudbeaver/core-theming'; export const topMenuStyles = composes( css` - Menu { + menu-box { composes: theme-text-on-primary from global; } @@ -21,7 +21,7 @@ export const topMenuStyles = composes( } `, css` - Menu { + menu-box { background-color: #338ecc!important; } MenuTrigger { @@ -55,7 +55,9 @@ export const topMenuStyles = composes( background: #236ea0; } } - MenuItem { + MenuItem, + MenuItemCheckbox, + MenuItemRadio { &:hover, &:global([aria-expanded="true"]) { background: #236ea0; } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index fded3c2dcb..e0a826b59a 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -6,8 +6,6 @@ * you may not use this file except in compliance with the License. */ -import { runInAction } from 'mobx'; - import { ConnectionAuthService, Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; @@ -17,9 +15,7 @@ import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/cor import { PermissionsService, EPermission, ServerService } from '@cloudbeaver/core-root'; -import { - resourceKeyList, ResourceKey, ResourceKeyUtils -} from '@cloudbeaver/core-sdk'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; import { NavigationService } from '@cloudbeaver/core-ui'; import { ENodeFeature } from './ENodeFeature'; @@ -116,11 +112,7 @@ export class NavNodeManagerService extends Bootstrap { .addHandler(this.navigateHandler.bind(this)); } - register(): void { - this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this)); - this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this)); - this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this)); - } + register(): void { } load(): void {} @@ -320,67 +312,6 @@ export class NavNodeManagerService extends Bootstrap { }; }; - private async connectionCreateHandler(connection: Connection) { - const enabled = await this.isNavTreeEnabled(); - if (!enabled) { - return; - } - - const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connection.id); - this.navTree.markTreeOutdated(nodeId); - - const tree = await this.navTree.load(ROOT_NODE_PATH); - - if (!tree.includes(nodeId)) { - await this.navTree.refresh(ROOT_NODE_PATH); - } - } - - private async connectionUpdateHandler(key: ResourceKey) { - const enabled = await this.isNavTreeEnabled(); - if (!enabled) { - return; - } - - await this.navTree.load(ROOT_NODE_PATH); - - runInAction(() => { - ResourceKeyUtils.forEach(key, async key => { - const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); - - if (this.navTree.has(nodeId)) { - const connectionInfo = this.connectionInfo.get(key); - - if (!connectionInfo?.connected) { - this.removeTree(nodeId); - } else { - this.navTree.markTreeOutdated(nodeId); - } - } - - const nodeInfo = this.navNodeInfoResource.get(nodeId); - - if (nodeInfo) { - this.navTree.markOutdated(nodeInfo.parentId); - } - }); - }); - } - - private connectionRemoveHandler(key: ResourceKey) { - runInAction(() => { - ResourceKeyUtils.forEach(key, key => { - const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); - - const node = this.getNode(navNodeId); - if (!node) { - return; - } - this.navTree.deleteInNode(node.parentId, [navNodeId]); - }); - }); - } - private async navigateHandler( data: INodeNavigationData, contexts: IExecutionContextProvider diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index a6ec51ffbd..7b89fbfdc6 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -6,8 +6,9 @@ * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable } from 'mobx'; +import { action, computed, makeObservable, runInAction } from 'mobx'; +import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { SessionDataResource } from '@cloudbeaver/core-root'; @@ -25,7 +26,8 @@ import { import { MetadataMap } from '@cloudbeaver/core-utils'; import { CoreSettingsService } from '../../CoreSettingsService'; -import { NavNodeInfoResource } from './NavNodeInfoResource'; +import { NavNodeInfoResource, ROOT_NODE_PATH } from './NavNodeInfoResource'; +import { NodeManagerUtils } from './NodeManagerUtils'; // TODO: so much dirty export interface NodePath { @@ -51,14 +53,16 @@ export class NavTreeResource extends CachedMapResource { private graphQLService: GraphQLService, private navNodeInfoResource: NavNodeInfoResource, private coreSettingsService: CoreSettingsService, - private sessionDataResource: SessionDataResource + private sessionDataResource: SessionDataResource, + private connectionInfo: ConnectionInfoResource ) { super(); - makeObservable(this, { + makeObservable(this, { childrenLimit: computed, setDetails: action, setNavObject: action, + connectionRemoveHandler: action.bound, }); this.metadata = new MetadataMap(() => ({ @@ -72,19 +76,22 @@ export class NavTreeResource extends CachedMapResource { this.onNodeRefresh = new Executor(null, (a, b) => a === b); this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource)); this.sessionDataResource.onDataUpdate.addPostHandler(() => this.markOutdated()); + this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this)); + this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler); + this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this)); } async refreshTree(navNodeId: string): Promise { await this.graphQLService.sdk.navRefreshNode({ nodePath: navNodeId, }); - this.markTreeOutdated(navNodeId); + await this.markTreeOutdated(navNodeId); await this.refresh(navNodeId); await this.onNodeRefresh.execute(navNodeId); } - markTreeOutdated(navNodeId: ResourceKey): void { - this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId))); + async markTreeOutdated(navNodeId: ResourceKey): Promise { + await this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId))); } setDetails(keyObject: ResourceKey, state: boolean): void { @@ -179,7 +186,7 @@ export class NavTreeResource extends CachedMapResource { const allKeys = resourceKeyList(items); this.markUpdated(allKeys); this.onItemDelete.execute(allKeys); - this.navNodeInfoResource.delete(resourceKeyList(items.filter(navNodeId => navNodeId !== key))); + this.navNodeInfoResource.delete(ResourceKeyUtils.exclude(allKeys, key)); } protected async loader(key: ResourceKey): Promise> { @@ -216,6 +223,50 @@ export class NavTreeResource extends CachedMapResource { return nestedChildren; } + private async connectionUpdateHandler(key: ResourceKey) { + await this.markOutdated(ROOT_NODE_PATH); + + ResourceKeyUtils.forEach(key, async key => { + const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); + + if (this.has(nodeId)) { + const connectionInfo = this.connectionInfo.get(key); + + if (!connectionInfo?.connected) { + this.delete(nodeId); + } else { + this.markTreeOutdated(nodeId); + } + } + + const node = this.navNodeInfoResource.get(nodeId); + + if (node) { + this.markOutdated(node.parentId); + } + }); + } + + private connectionRemoveHandler(key: ResourceKey) { + runInAction(() => { + ResourceKeyUtils.forEach(key, key => { + const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); + + const node = this.navNodeInfoResource.get(nodeId); + + if (node) { + this.deleteInNode(node.parentId, [nodeId]); + } + }); + }); + } + + private async connectionCreateHandler(connection: Connection) { + const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connection.id); + await this.markOutdated(ROOT_NODE_PATH); + await this.markTreeOutdated(nodeId); + } + private setNavObject(data: NavNodeChildrenQuery | NavNodeChildrenQuery[]) { if (Array.isArray(data)) { for (const node of data) { diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index e0fc3e2e37..c615552084 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -13,7 +13,7 @@ import { use } from 'reshadow'; import { useStyles, composes, ComponentStyle } from '@cloudbeaver/core-theming'; -import { Icon } from './Icons'; +import { Icon } from './Icon'; import { Loader } from './Loader/Loader'; import { useObjectRef } from './useObjectRef'; diff --git a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts index 24eecfdc2e..523c998563 100644 --- a/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts +++ b/webapp/packages/core-blocks/src/Containers/BASE_CONTAINERS_STYLES.ts @@ -24,6 +24,7 @@ export const BASE_CONTAINERS_STYLES = composes( display: flex; flex-direction: row; align-content: baseline; + position: relative; &[vertical] { flex-direction: column; @@ -129,5 +130,15 @@ export const BASE_CONTAINERS_STYLES = composes( margin: 0; text-transform: uppercase; opacity: 0.9; - } + } + + GroupClose { + width: 18px; + height: 18px; + cursor: pointer; + display: flex; + position: absolute; + right: 24px; + margin-right: 0 !important; + } `); diff --git a/webapp/packages/core-blocks/src/Containers/GroupClose.tsx b/webapp/packages/core-blocks/src/Containers/GroupClose.tsx new file mode 100644 index 0000000000..72398fd077 --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/GroupClose.tsx @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { Icon } from '../Icon'; + +interface IProps { + onClick?: () => void; +} + +export const GroupClose: React.FC> = function GroupClose({ + onClick, + ...rest +}) { + return ; +}; diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 4266e07895..232ec9dfd9 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -20,8 +20,8 @@ import styled, { css, use } from 'reshadow'; import { useStyles, composes } from '@cloudbeaver/core-theming'; +import { Icon } from '../Icon'; import { IconButton } from '../IconButton'; -import { Icon } from '../Icons/Icon'; import { baseFormControlStyles } from './baseFormControlStyles'; import { FormContext } from './FormContext'; diff --git a/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx index bd735691c6..2af38bdfe7 100644 --- a/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/ComboboxNew.tsx @@ -21,8 +21,8 @@ import styled, { css, use } from 'reshadow'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; +import { Icon } from '../Icon'; import { IconButton } from '../IconButton'; -import { Icon } from '../Icons/Icon'; import { baseFormControlStylesNew } from './baseFormControlStylesNew'; import { FormContext } from './FormContext'; diff --git a/webapp/packages/core-blocks/src/Icons/Icon.tsx b/webapp/packages/core-blocks/src/Icon.tsx similarity index 100% rename from webapp/packages/core-blocks/src/Icons/Icon.tsx rename to webapp/packages/core-blocks/src/Icon.tsx diff --git a/webapp/packages/core-blocks/src/IconButton.tsx b/webapp/packages/core-blocks/src/IconButton.tsx index d9453fce41..c867a49ebc 100644 --- a/webapp/packages/core-blocks/src/IconButton.tsx +++ b/webapp/packages/core-blocks/src/IconButton.tsx @@ -9,7 +9,7 @@ import { Button, ButtonProps } from 'reakit/Button'; import styled, { css } from 'reshadow'; -import { Icon } from './Icons/Icon'; +import { Icon } from './Icon'; const styles = css` Button { diff --git a/webapp/packages/core-blocks/src/IconOrImage.tsx b/webapp/packages/core-blocks/src/IconOrImage.tsx index 73e8924828..b780eebdde 100644 --- a/webapp/packages/core-blocks/src/IconOrImage.tsx +++ b/webapp/packages/core-blocks/src/IconOrImage.tsx @@ -8,7 +8,7 @@ import { useMemo } from 'react'; -import { Icon } from './Icons'; +import { Icon } from './Icon'; import { StaticImage } from './StaticImage'; export interface IconOrImageProps { diff --git a/webapp/packages/core-blocks/src/Icons/index.ts b/webapp/packages/core-blocks/src/Icons/index.ts deleted file mode 100644 index e263cc0e6d..0000000000 --- a/webapp/packages/core-blocks/src/Icons/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Icon'; diff --git a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx index 085873b581..679aa343c4 100644 --- a/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx +++ b/webapp/packages/core-blocks/src/PropertiesTable/PropertyItem.tsx @@ -15,7 +15,7 @@ import styled, { css, use } from 'reshadow'; import { composes, useStyles } from '@cloudbeaver/core-theming'; import { ShadowInput } from '../FormControls/ShadowInput'; -import { Icon } from '../Icons'; +import { Icon } from '../Icon'; import type { IProperty } from './IProperty'; import { PropertyValueSelector } from './PropertyValueSelector'; diff --git a/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx b/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx index 7199c5b859..5241ad9a15 100644 --- a/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx +++ b/webapp/packages/core-blocks/src/Slide/SlideOverlay.tsx @@ -10,7 +10,7 @@ import styled, { css } from 'reshadow'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import { Icon } from '../Icons'; +import { Icon } from '../Icon'; interface Props { className?: string; diff --git a/webapp/packages/core-blocks/src/Table/TableItemExpand.tsx b/webapp/packages/core-blocks/src/Table/TableItemExpand.tsx index 096ef92f19..da6fa24167 100644 --- a/webapp/packages/core-blocks/src/Table/TableItemExpand.tsx +++ b/webapp/packages/core-blocks/src/Table/TableItemExpand.tsx @@ -12,7 +12,7 @@ import styled, { use } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; -import { Icon } from '../Icons/Icon'; +import { Icon } from '../Icon'; import { TableContext } from './TableContext'; import { TableItemContext } from './TableItemContext'; diff --git a/webapp/packages/core-blocks/src/Tabs/Tab/Tab.tsx b/webapp/packages/core-blocks/src/Tabs/Tab/Tab.tsx index b7ffd6ce16..a33a2584be 100644 --- a/webapp/packages/core-blocks/src/Tabs/Tab/Tab.tsx +++ b/webapp/packages/core-blocks/src/Tabs/Tab/Tab.tsx @@ -14,7 +14,7 @@ import styled from 'reshadow'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; -import { Icon } from '../../Icons'; +import { Icon } from '../../Icon'; import { TabContext } from '../TabContext'; import type { TabProps } from './TabProps'; import { useTab } from './useTab'; diff --git a/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx b/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx index 2dc7767082..95d4600108 100644 --- a/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx +++ b/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx @@ -51,7 +51,7 @@ interface Props extends ButtonHTMLAttributes { export const ToolsAction: React.FC = function ToolsAction({ icon, viewBox, children, ...rest }) { return styled(useStyles(styles))( - diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 2fbf33197e..dbc4da9f6c 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -1,5 +1,5 @@ export * from './ComplexLoader'; -export * from './Icons'; +export * from './Icon'; export * from './ItemList/ItemList'; export * from './ItemList/ItemListSearch'; @@ -99,6 +99,7 @@ export * from './FormControls/Filter'; export * from './Containers/BASE_CONTAINERS_STYLES'; export * from './Containers/Container'; export * from './Containers/Group'; +export * from './Containers/GroupClose'; export * from './Containers/GroupItem'; export * from './Containers/GroupTitle'; export * from './Containers/ColoredContainer'; diff --git a/webapp/packages/core-blocks/src/useMouse.ts b/webapp/packages/core-blocks/src/useMouse.ts index bc4e6ad046..786da39604 100644 --- a/webapp/packages/core-blocks/src/useMouse.ts +++ b/webapp/packages/core-blocks/src/useMouse.ts @@ -41,7 +41,9 @@ export function useMouse(options: IOptions): IMouseHook { @@ -49,7 +51,9 @@ export function useMouse(options: IOptions): IMouseHook implements IComputedMenuItemOptions isChecked?: () => boolean; type?: MenuItemType; separator?: boolean; + keepMenuOpen?: boolean; iconGetter?: () => string | undefined; constructor(private options: IContextMenuItem, @@ -112,6 +113,7 @@ class ComputedMenuItemOptionsWithContext implements IComputedMenuItemOptions this.icon = options.icon; this.type = options.type; this.separator = options.separator; + this.keepMenuOpen = options.keepMenuOpen; this.iconGetter = options.iconGetter; this.id = `${options.id}-${context.contextId!}`; diff --git a/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts b/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts index dfff022226..77b0c90969 100644 --- a/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts +++ b/webapp/packages/core-dialogs/src/Menu/IMenuPanel.ts @@ -28,6 +28,7 @@ export interface IMenuItem { onClick?: () => void; // it is not mandatory if it is just opens submenu isDisabled?: boolean; isHidden?: boolean; + keepMenuOpen?: boolean; icon?: string; // path to icon or svg icon name tooltip?: string; panel?: IMenuPanel; // if menu has sub-items @@ -42,5 +43,6 @@ export type MenuTriggerProps = PropsWithChildren<{ placement?: MenuInitialState['placement']; modal?: boolean; visible?: boolean; + rtl?: boolean; onVisibleSwitch?: (visible: boolean) => void; }> & Omit, 'style'>; diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx index 747ef69949..27dc5de330 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx @@ -44,16 +44,16 @@ export const MenuPanelItem: React.FC = function MenuPanelIte } return styled(useStyles(menuPanelStyles, style))( - - + + {icon ? ( ) : control} - + - + {panel && } diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx index c905d693f2..a0ab6ed8ee 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx @@ -36,10 +36,15 @@ export const MenuTrigger: React.FC = function MenuTrigger({ visible, onVisibleSwitch, modal, + rtl, ...props }) { const propsRef = useObjectRef({ onVisibleSwitch, visible }, { onVisibleSwitch }); - const menu = useMenuState({ modal, placement, visible }); + const menu = useMenuState({ modal, placement, visible, rtl }); + + const handleItemClose = useCallback(() => { + menu.hide(); + }, [menu.hide]); useEffect(() => { propsRef.onVisibleSwitch?.(menu.visible); @@ -48,11 +53,9 @@ export const MenuTrigger: React.FC = function MenuTrigger({ return styled(useStyles(menuPanelStyles, ...style))( <> - - {children} - + {children} - + ); }; @@ -64,12 +67,16 @@ export const MenuTrigger: React.FC = function MenuTrigger({ interface MenuPanelProps { panel: IMenuPanel; menu: MenuStateReturn; // from reakit useMenuState + onItemClose?: () => void; + rtl?: boolean; style?: Style[]; } const MenuPanel = observer(function MenuPanel({ panel, menu, + rtl, + onItemClose, style = [], }: MenuPanelProps) { const styles = useStyles(menuPanelStyles, ...style); @@ -79,10 +86,12 @@ const MenuPanel = observer(function MenuPanel({ } return styled(styles)( - - {panel.menuItems.map(item => ( - - ))} + + + {panel.menuItems.map(item => ( + + ))} + ); }); @@ -94,21 +103,22 @@ const MenuPanel = observer(function MenuPanel({ type MenuPanelElementProps = Omit, 'style'> & { item: IMenuItem; menu: MenuStateReturn; // from reakit useMenuState + onItemClose?: () => void; style?: Style[]; }; const MenuPanelElement = observer(function MenuPanelElement({ - item, menu, style = [], + item, menu, onItemClose, style = [], }: MenuPanelElementProps) { const styles = useStyles(menuPanelStyles, ...style); const onClick = useCallback(() => { if (item.onClick) { item.onClick(); } - if (!item.panel) { - menu.hide(); + if (!item.keepMenuOpen && !item.panel) { + onItemClose?.(); } - }, [item, menu]); + }, [item, menu, onItemClose]); if (item.panel) { return styled(styles)( @@ -119,6 +129,7 @@ const MenuPanelElement = observer(function MenuPanelElement({ disabled={item.isDisabled} menuItem={item} style={style} + onItemClose={onItemClose} onClick={onClick} {...{ as: MenuInnerTrigger }} /> @@ -180,6 +191,7 @@ const MenuPanelElement = observer(function MenuPanelElement({ type MenuInnerTriggerProps = Omit, 'style'> & { menuItem: IMenuItem; + onItemClose?: () => void; style?: Style[]; }; @@ -193,14 +205,19 @@ export const MenuInnerTrigger = forwardRef(function MenuInnerTrigger( const menu = useMenuState(); const panel = useObserver(() => menuItem.panel); + const handleItemClose = useCallback(() => { + menu.hide(); + props.onItemClose?.(); + }, [menu.hide, props.onItemClose]); + return styled(useStyles(menuPanelStyles, ...style))( <> - + - + ); }); diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts b/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts index 528b96341c..68323f466e 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/menuPanelStyles.ts @@ -12,7 +12,7 @@ import { composes } from '@cloudbeaver/core-theming'; export const menuPanelStyles = composes( css` - Menu { + menu-box { composes: theme-background-surface theme-text-on-surface from global; } MenuItem, @@ -43,16 +43,17 @@ export const menuPanelStyles = composes( } } Menu { + outline: none; + z-index: 999; + } + menu-box { composes: theme-typography--body2 theme-elevation-z5 from global; - position: absolute; display: flex; min-width: 140px; flex-direction: column; padding: 12px 0; - outline: none; - z-index: 999; - & Menu { + & menu-box { margin-top: -12px; } } diff --git a/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts b/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts index c71025337e..36f6b3ea32 100644 --- a/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts +++ b/webapp/packages/core-dialogs/src/Menu/models/ComputedMenuItemModel.ts @@ -26,6 +26,7 @@ export class ComputedMenuItemModel implements IMenuItem { panel?: IMenuPanel; type?: MenuItemType; separator?: boolean; + keepMenuOpen?: boolean; rtl?: boolean; get title(): TLocalizationToken { @@ -73,6 +74,7 @@ export class ComputedMenuItemModel implements IMenuItem { this.id = options.id; this.type = options.type; + this.keepMenuOpen = options.keepMenuOpen; this.separator = options.separator; this.rtl = options.rtl; this.panel = options.panel; diff --git a/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts b/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts index 918ac4cf9d..1c93e008f8 100644 --- a/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts +++ b/webapp/packages/core-dialogs/src/Menu/models/MenuOptionsStore.ts @@ -26,6 +26,7 @@ export interface IMenuItemOptions { tooltipGetter?: () => TLocalizationToken | undefined; order?: number; isPanel?: boolean; + keepMenuOpen?: boolean; panel?: IMenuPanel; type?: MenuItemType; separator?: boolean; diff --git a/webapp/packages/core-localization/src/LocalizationService.ts b/webapp/packages/core-localization/src/LocalizationService.ts index 0f283a5b2f..1f8287b8a9 100644 --- a/webapp/packages/core-localization/src/LocalizationService.ts +++ b/webapp/packages/core-localization/src/LocalizationService.ts @@ -10,8 +10,8 @@ import { action, observable, makeObservable } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { SessionService, ServerService } from '@cloudbeaver/core-root'; -import { GraphQLService, ServerLanguage } from '@cloudbeaver/core-sdk'; +import { ServerService, SessionResource, SessionDataResource } from '@cloudbeaver/core-root'; +import type { ServerLanguage } from '@cloudbeaver/core-sdk'; import { SettingsService } from '@cloudbeaver/core-settings'; import type { ILocaleProvider } from './ILocaleProvider'; @@ -37,9 +37,9 @@ export class LocalizationService extends Bootstrap { constructor( private notificationService: NotificationService, - private sessionService: SessionService, + private sessionResource: SessionResource, + sessionDataResource: SessionDataResource, private serverService: ServerService, - private graphQLService: GraphQLService, private settingsService: SettingsService ) { super(); @@ -49,6 +49,8 @@ export class LocalizationService extends Bootstrap { localeMap: observable.shallow, setCurrentLocale: action, }); + + sessionDataResource.onDataUpdate.addHandler(this.syncLanguage.bind(this)); } addProvider(provider: ILocaleProvider): void { @@ -78,20 +80,12 @@ export class LocalizationService extends Bootstrap { register(): void | Promise { this.addProvider(this.coreProvider.bind(this)); + this.settingsService.registerSettings(this.settings, LANG_SETTINGS_KEY); // overwrite default value with settings } async load(): Promise { - const session = await this.sessionService.session.load(); await this.loadLocaleAsync(DEFAULT_LOCALE_NAME); - - if (!session) { - return; - } - - this.settingsService.registerSettings(this.settings, LANG_SETTINGS_KEY); // overwrite default value with settings - this.setCurrentLocale(session.locale); // session language wins - - await this.setLocale(this.getCurrentLanguage()); + await this.loadLocaleAsync(this.settings.language); } getCurrentLanguage(): string { @@ -102,10 +96,16 @@ export class LocalizationService extends Bootstrap { if (key === this.settings.language) { return; } - const response = await this.graphQLService.sdk.changeSessionLanguage({ locale: key }); - this.setLocale(key); - if (response.changeSessionLanguage) { - window.location.reload(); + await this.sessionResource.changeLanguage(key); + // window.location.reload(); + } + + private async syncLanguage() { + await this.sessionResource.refreshSilent(); // TODO: remove + const session = this.sessionResource.data; + + if (session) { + await this.setLocale(session.locale); } } @@ -126,13 +126,15 @@ export class LocalizationService extends Bootstrap { const config = await this.serverService.config.load(); if (!config) { - throw new Error('Cant\'t get server settings'); + throw new Error('Can\'t get server settings'); } if (!config.supportedLanguages.some(lang => lang.isoCode === key)) { this.setCurrentLocale(config!.supportedLanguages[0]!.isoCode); throw new Error(`Language '${key}' is not supported`); } + + this.setCurrentLocale(key); await this.loadLocaleAsync(key); } diff --git a/webapp/packages/core-root/src/SessionResource.ts b/webapp/packages/core-root/src/SessionResource.ts index 531e9d4f71..f39705152f 100644 --- a/webapp/packages/core-root/src/SessionResource.ts +++ b/webapp/packages/core-root/src/SessionResource.ts @@ -28,6 +28,22 @@ export class SessionResource extends CachedDataResource { + const { session } = await this.graphQLService.sdk.openSession(); + + this.data = session; + } + + async changeLanguage(locale: string): Promise { + await this.performUpdate(undefined, undefined, async () => { + await this.graphQLService.sdk.changeSessionLanguage({ locale }); + + if (this.data) { + this.data.locale = locale; + } + }); + } + protected async loader(): Promise { const { session } = await this.graphQLService.sdk.openSession(); diff --git a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts index c692f15f42..cd45d38608 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts @@ -46,6 +46,7 @@ export interface ResourceKeyUtils { every: (key: ResourceKey, predicate: (key: TKey, index: number) => boolean) => boolean; map: MapFnc; includes: (first: ResourceKey, second: ResourceKey) => boolean; + exclude: (first: ResourceKeyList, second: ResourceKey) => ResourceKey; } // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -142,6 +143,14 @@ export const ResourceKeyUtils: ResourceKeyUtils = { return param === key; }, + + exclude(param: ResourceKeyList, key: ResourceKey): ResourceKey { + if (isResourceKeyList(key)) { + return resourceKeyList(param.list.filter(param => !key.list.includes(param)), param.mark); + } + + return resourceKeyList(param.list.filter(param => param !== key), param.mark); + }, }; export function isResourceKeyList(data: any): data is ResourceKeyList { diff --git a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql index 79657fe94d..036d348752 100644 --- a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql +++ b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql @@ -3,6 +3,7 @@ query serverConfig { name version workspaceId + serverURL productConfiguration supportsCustomConnections supportsConnectionBrowser diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 5bfd935454..572765534d 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -516,6 +516,7 @@ export interface ServerConfig { name: Scalars['String']; version: Scalars['String']; workspaceId: Scalars['ID']; + serverURL: Scalars['String']; anonymousAccessEnabled?: Maybe; /** @deprecated Field no longer supported */ authenticationEnabled?: Maybe; @@ -952,6 +953,7 @@ export interface AdminPermissionInfo { export interface ServerConfigInput { serverName?: Maybe; + serverURL?: Maybe; adminName?: Maybe; adminPassword?: Maybe; anonymousAccessEnabled?: Maybe; @@ -1678,7 +1680,7 @@ export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>; export interface ServerConfigQuery { serverConfig: ( - Pick + Pick & { supportedLanguages: Array>; defaultNavigatorSettings: AllNavigatorSettingsFragment; productInfo: Pick } ); } @@ -2691,6 +2693,7 @@ export const ServerConfigDocument = ` name version workspaceId + serverURL productConfiguration supportsCustomConnections supportsConnectionBrowser diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx index 0c54cf22d6..750926d2e8 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx @@ -37,6 +37,17 @@ export const ServerConfigurationInfoForm: React.FC = observer(function Se > {translate('administration_configuration_wizard_configuration_server_name')} + + {translate('administration_configuration_wizard_configuration_server_url')} + + diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx index 1706126dc0..bd96959ac1 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridLoader.tsx @@ -6,8 +6,6 @@ * you may not use this file except in compliance with the License. */ -import { observer } from 'mobx-react-lite'; - import { ComplexLoader, createComplexLoader, Loader } from '@cloudbeaver/core-blocks'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; @@ -22,7 +20,7 @@ const loader = createComplexLoader(async function loader() { return { DataGridTable }; }); -export const DataGridLoader: React.FC = observer(function DataGridLoader({ +export const DataGridLoader: React.FC = function DataGridLoader({ tableModel, resultIndex, className, }) { return ( @@ -35,4 +33,4 @@ export const DataGridLoader: React.FC = observer(function DataGridLoader( )} ); -}); +}; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx index c608a0893e..902c142194 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx @@ -58,6 +58,7 @@ export const CellMenu: React.FC = observer(function TreeNodeMenu({ dataGridContextMenuService.openMenu(model, resultIndex, row, column)} onVisibleSwitch={onStateSwitch} > diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx index 24c67747ab..05c437daed 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx @@ -86,6 +86,7 @@ export const TableFooterMenuItem = observer(function TableFooterMenuItem({ panel={menuItem.panel} disabled={menuItem.isDisabled} style={[tableFooterMenuStyles]} + modal > {menuItem.icon && ( diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTab.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTab.tsx index 75b06275e0..f5605e074b 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTab.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTab.tsx @@ -18,7 +18,6 @@ import { VirtualFolderTabMixin } from './VirtualFolderTabMixin'; const style = css` tab-wrapper { position: relative; - display: flex; width: 100%; flex: 1 0 auto; } @@ -32,7 +31,7 @@ export const VirtualFolderTab = observer(function VirtualFolderTab() { } return styled(style)( - + );