mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge branch 'devel' into feat/execution-plan
This commit is contained in:
@@ -90,6 +90,8 @@ type ServerConfig {
|
||||
version: String!
|
||||
workspaceId: ID!
|
||||
|
||||
serverURL: String!
|
||||
|
||||
anonymousAccessEnabled: Boolean
|
||||
authenticationEnabled: Boolean @deprecated
|
||||
supportsCustomConnections: Boolean
|
||||
|
||||
@@ -28,7 +28,7 @@ type SQLCompletionProposal {
|
||||
replacementString: String!
|
||||
replacementOffset: Int!
|
||||
replacementLength: Int!
|
||||
cursorPosition: Int!
|
||||
cursorPosition: Int
|
||||
|
||||
icon: String
|
||||
nodePath: String
|
||||
|
||||
@@ -45,6 +45,17 @@ public interface DBWAuthProviderExternal<AUTH_SESSION extends DBASession> extend
|
||||
@NotNull Map<String, Object> 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<String, Object> authFederatedUser(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull Map<String, Object> providerConfig, // Auth provider configuration (e.g. 3rd party auth server address)
|
||||
@NotNull String faProvider,
|
||||
@NotNull Map<String, Object> 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
|
||||
|
||||
+10
@@ -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<SESSION extends DBASession> i
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<String, Object> authFederatedUser(@NotNull DBRProgressMonitor monitor, @NotNull Map<String, Object> providerConfig, @NotNull String faProvider, @NotNull Map<String, Object> faParameters) throws DBException {
|
||||
throw new DBException("Federated authorization not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String, Object> plugins = new LinkedHashMap<>();
|
||||
private Map<String, Object> authConfiguration = new LinkedHashMap<>();
|
||||
|
||||
public boolean isAnonymousAccessEnabled() {
|
||||
return anonymousAccessEnabled;
|
||||
@@ -132,15 +134,16 @@ public class CBAppConfig {
|
||||
this.defaultNavigatorSettings = new DataSourceNavigatorSettings(defaultNavigatorSettings);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<String, Object> getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public Map<String, Object> getPluginConfig(String pluginId) {
|
||||
public Map<String, Object> getPluginConfig(@NotNull String pluginId) {
|
||||
return getPluginConfig(pluginId, false);
|
||||
}
|
||||
|
||||
public Map<String, Object> getPluginConfig(String pluginId, boolean create) {
|
||||
public Map<String, Object> getPluginConfig(@NotNull String pluginId, boolean create) {
|
||||
Object config = plugins.get(pluginId);
|
||||
if (config instanceof Map) {
|
||||
return (Map<String, Object>) config;
|
||||
@@ -155,8 +158,21 @@ public class CBAppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T getPluginOption(String pluginId, String option) {
|
||||
public <T> T getPluginOption(@NotNull String pluginId, @NotNull String option) {
|
||||
return (T)getPluginConfig(pluginId).get(option);
|
||||
}
|
||||
|
||||
public Map<String, Object> getAuthConfiguration() {
|
||||
return authConfiguration;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<String, Object> getAuthConfiguration(@NotNull String providerId) {
|
||||
Object apConfig = authConfiguration.get(providerId);
|
||||
if (apConfig instanceof Map) {
|
||||
return (Map<String, Object>) apConfig;
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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<WebAuthInfo> 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();
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+3
-2
@@ -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());
|
||||
|
||||
@@ -50,6 +50,7 @@ type AdminPermissionInfo {
|
||||
|
||||
input ServerConfigInput {
|
||||
serverName: String
|
||||
serverURL: String
|
||||
|
||||
adminName: String
|
||||
adminPassword: String
|
||||
|
||||
+10
@@ -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<String, Object> 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;
|
||||
}
|
||||
|
||||
+1
@@ -419,6 +419,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
|
||||
CBApplication.getInstance().finishConfiguration(
|
||||
config.getServerName(),
|
||||
config.getServerURL(),
|
||||
adminName,
|
||||
adminPassword,
|
||||
authInfoList,
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ export const TreeNodeMenu = observer(function TreeNodeMenu({
|
||||
}
|
||||
|
||||
return styled(treeNodeMenuStyles)(
|
||||
<MenuTrigger panel={menuPanel} {...use({ selected })}>
|
||||
<MenuTrigger panel={menuPanel} {...use({ selected })} modal>
|
||||
<Icon name="snack" viewBox="0 0 16 10" />
|
||||
</MenuTrigger>
|
||||
);
|
||||
|
||||
@@ -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<boolean> {
|
||||
async loadNestedNodes(id = ROOT_NODE_PATH, tryConnect?: boolean): Promise<boolean> {
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const MainMenu = observer(function MainMenu() {
|
||||
const mainMenuService = useService(MainMenuService);
|
||||
|
||||
return styled(styles)(
|
||||
<menu-wrapper as="div">
|
||||
<menu-wrapper>
|
||||
{mainMenuService.getMainMenu().map((topItem, i) => (
|
||||
<TopMenuItem key={i} menuItem={topItem} />
|
||||
))}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const SettingsMenu = observer(function SettingsMenu() {
|
||||
const settingsMenuService = useService(SettingsMenuService);
|
||||
|
||||
return styled(useStyles(settingsMenuStyles))(
|
||||
<MenuTrigger panel={settingsMenuService.getMenu()} style={[topMenuStyles, settingsMenuStyles]}>
|
||||
<MenuTrigger panel={settingsMenuService.getMenu()} style={[topMenuStyles, settingsMenuStyles]} placement='bottom-end' modal rtl>
|
||||
<Icon name="settings" viewBox="0 0 28 28" />
|
||||
</MenuTrigger>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -25,7 +25,8 @@ type TopMenuItemProps = Omit<ButtonHTMLAttributes<any>, '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 && (
|
||||
<menu-trigger-icon as="div">
|
||||
<menu-trigger-icon>
|
||||
<IconOrImage icon={menuItem.icon} />
|
||||
</menu-trigger-icon>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string>) {
|
||||
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<string>) {
|
||||
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<INodeNavigationData>
|
||||
|
||||
@@ -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<string, string[]> {
|
||||
private graphQLService: GraphQLService,
|
||||
private navNodeInfoResource: NavNodeInfoResource,
|
||||
private coreSettingsService: CoreSettingsService,
|
||||
private sessionDataResource: SessionDataResource
|
||||
private sessionDataResource: SessionDataResource,
|
||||
private connectionInfo: ConnectionInfoResource
|
||||
) {
|
||||
super();
|
||||
|
||||
makeObservable<NavTreeResource, 'setNavObject'>(this, {
|
||||
makeObservable<NavTreeResource, 'setNavObject' | 'connectionRemoveHandler'>(this, {
|
||||
childrenLimit: computed,
|
||||
setDetails: action,
|
||||
setNavObject: action,
|
||||
connectionRemoveHandler: action.bound,
|
||||
});
|
||||
|
||||
this.metadata = new MetadataMap<string, INodeMetadata>(() => ({
|
||||
@@ -72,19 +76,22 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
this.onNodeRefresh = new Executor<string>(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<void> {
|
||||
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<string>): void {
|
||||
this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId)));
|
||||
async markTreeOutdated(navNodeId: ResourceKey<string>): Promise<void> {
|
||||
await this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId)));
|
||||
}
|
||||
|
||||
setDetails(keyObject: ResourceKey<string>, state: boolean): void {
|
||||
@@ -179,7 +186,7 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
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<string>): Promise<Map<string, string[]>> {
|
||||
@@ -216,6 +223,50 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
|
||||
return nestedChildren;
|
||||
}
|
||||
|
||||
private async connectionUpdateHandler(key: ResourceKey<string>) {
|
||||
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<string>) {
|
||||
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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -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<IProps & React.HTMLAttributes<HTMLDivElement>> = function GroupClose({
|
||||
onClick,
|
||||
...rest
|
||||
}) {
|
||||
return <close {...rest}><Icon name="cross" viewBox="0 0 16 16" onClick={onClick} /></close>;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Icon } from './Icons';
|
||||
import { Icon } from './Icon';
|
||||
import { StaticImage } from './StaticImage';
|
||||
|
||||
export interface IconOrImageProps {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Icon';
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -51,7 +51,7 @@ interface Props extends ButtonHTMLAttributes<any> {
|
||||
|
||||
export const ToolsAction: React.FC<Props> = function ToolsAction({ icon, viewBox, children, ...rest }) {
|
||||
return styled(useStyles(styles))(
|
||||
<button {...rest}>
|
||||
<button type='button' {...rest}>
|
||||
{icon && <IconOrImage icon={icon} viewBox={viewBox} />}
|
||||
{children && <button-label>{children}</button-label>}
|
||||
</button>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -41,7 +41,9 @@ export function useMouse<T extends HTMLElement>(options: IOptions): IMouseHook<T
|
||||
handlersRef.onMouseEnter(event);
|
||||
}
|
||||
|
||||
state.mouseEnter = true;
|
||||
if (!state.mouseEnter) {
|
||||
state.mouseEnter = true;
|
||||
}
|
||||
};
|
||||
|
||||
const mouseOutHandler = (event: MouseEvent) => {
|
||||
@@ -49,7 +51,9 @@ export function useMouse<T extends HTMLElement>(options: IOptions): IMouseHook<T
|
||||
handlersRef.onMouseLeave(event);
|
||||
}
|
||||
|
||||
state.mouseEnter = false;
|
||||
if (state.mouseEnter) {
|
||||
state.mouseEnter = false;
|
||||
}
|
||||
};
|
||||
|
||||
const element = reference.current;
|
||||
|
||||
@@ -101,6 +101,7 @@ class ComputedMenuItemOptionsWithContext<T> implements IComputedMenuItemOptions
|
||||
isChecked?: () => boolean;
|
||||
type?: MenuItemType;
|
||||
separator?: boolean;
|
||||
keepMenuOpen?: boolean;
|
||||
iconGetter?: () => string | undefined;
|
||||
|
||||
constructor(private options: IContextMenuItem<T>,
|
||||
@@ -112,6 +113,7 @@ class ComputedMenuItemOptionsWithContext<T> 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!}`;
|
||||
|
||||
@@ -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<ButtonHTMLAttributes<any>, 'style'>;
|
||||
|
||||
@@ -44,16 +44,16 @@ export const MenuPanelItem: React.FC<MenuPanelItemProps> = function MenuPanelIte
|
||||
}
|
||||
|
||||
return styled(useStyles(menuPanelStyles, style))(
|
||||
<menu-panel-item as="div" {...use({ separator })}>
|
||||
<menu-item-content as='div'>
|
||||
<menu-panel-item {...use({ separator })}>
|
||||
<menu-item-content>
|
||||
{icon ? (
|
||||
<IconOrImage icon={icon} />
|
||||
) : control}
|
||||
</menu-item-content>
|
||||
<menu-item-text as="div">
|
||||
<menu-item-text>
|
||||
<Translate token={title} />
|
||||
</menu-item-text>
|
||||
<menu-item-content as='div'>
|
||||
<menu-item-content>
|
||||
{panel && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
</menu-item-content>
|
||||
</menu-panel-item>
|
||||
|
||||
@@ -36,10 +36,15 @@ export const MenuTrigger: React.FC<MenuTriggerProps> = 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<MenuTriggerProps> = function MenuTrigger({
|
||||
return styled(useStyles(menuPanelStyles, ...style))(
|
||||
<>
|
||||
<MenuButton {...menu} {...props}>
|
||||
<box as='div'>
|
||||
{children}
|
||||
</box>
|
||||
<box>{children}</box>
|
||||
</MenuButton>
|
||||
<MenuPanel panel={panel} menu={menu} style={style} />
|
||||
<MenuPanel panel={panel} menu={menu} style={style} rtl={rtl} onItemClose={handleItemClose} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -64,12 +67,16 @@ export const MenuTrigger: React.FC<MenuTriggerProps> = 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)(
|
||||
<Menu {...menu} aria-label={panel.id} modal>
|
||||
{panel.menuItems.map(item => (
|
||||
<MenuPanelElement key={item.id} item={item} menu={menu} style={style} />
|
||||
))}
|
||||
<Menu {...menu} aria-label={panel.id}>
|
||||
<menu-box dir={rtl ? 'rtl' : undefined}>
|
||||
{panel.menuItems.map(item => (
|
||||
<MenuPanelElement key={item.id} item={item} menu={menu} style={style} onItemClose={onItemClose} />
|
||||
))}
|
||||
</menu-box>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
@@ -94,21 +103,22 @@ const MenuPanel = observer(function MenuPanel({
|
||||
type MenuPanelElementProps = Omit<React.ButtonHTMLAttributes<any>, '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<React.ButtonHTMLAttributes<any>, '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))(
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...rest}>
|
||||
<box as='div'>
|
||||
<box>
|
||||
<MenuPanelItem menuItem={menuItem} style={style} />
|
||||
</box>
|
||||
</MenuButton>
|
||||
<MenuPanel panel={panel!} menu={menu} style={style} />
|
||||
<MenuPanel panel={panel!} menu={menu} style={style} onItemClose={handleItemClose} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface IMenuItemOptions {
|
||||
tooltipGetter?: () => TLocalizationToken | undefined;
|
||||
order?: number;
|
||||
isPanel?: boolean;
|
||||
keepMenuOpen?: boolean;
|
||||
panel?: IMenuPanel;
|
||||
type?: MenuItemType;
|
||||
separator?: boolean;
|
||||
|
||||
@@ -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<void> {
|
||||
this.addProvider(this.coreProvider.bind(this));
|
||||
this.settingsService.registerSettings(this.settings, LANG_SETTINGS_KEY); // overwrite default value with settings
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,22 @@ export class SessionResource extends CachedDataResource<SessionState | null, voi
|
||||
this.sync(serverConfiguration);
|
||||
}
|
||||
|
||||
async refreshSilent(): Promise<void> {
|
||||
const { session } = await this.graphQLService.sdk.openSession();
|
||||
|
||||
this.data = session;
|
||||
}
|
||||
|
||||
async changeLanguage(locale: string): Promise<void> {
|
||||
await this.performUpdate(undefined, undefined, async () => {
|
||||
await this.graphQLService.sdk.changeSessionLanguage({ locale });
|
||||
|
||||
if (this.data) {
|
||||
this.data.locale = locale;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async loader(): Promise<SessionState> {
|
||||
const { session } = await this.graphQLService.sdk.openSession();
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ResourceKeyUtils {
|
||||
every: <TKey>(key: ResourceKey<TKey>, predicate: (key: TKey, index: number) => boolean) => boolean;
|
||||
map: MapFnc;
|
||||
includes: <TKey>(first: ResourceKey<TKey>, second: ResourceKey<TKey>) => boolean;
|
||||
exclude: <TKey>(first: ResourceKeyList<TKey>, second: ResourceKey<TKey>) => ResourceKey<TKey>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
@@ -142,6 +143,14 @@ export const ResourceKeyUtils: ResourceKeyUtils = {
|
||||
|
||||
return param === key;
|
||||
},
|
||||
|
||||
exclude<TKey>(param: ResourceKeyList<TKey>, key: ResourceKey<TKey>): ResourceKey<TKey> {
|
||||
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<T>(data: any): data is ResourceKeyList<T> {
|
||||
|
||||
@@ -3,6 +3,7 @@ query serverConfig {
|
||||
name
|
||||
version
|
||||
workspaceId
|
||||
serverURL
|
||||
productConfiguration
|
||||
supportsCustomConnections
|
||||
supportsConnectionBrowser
|
||||
|
||||
@@ -516,6 +516,7 @@ export interface ServerConfig {
|
||||
name: Scalars['String'];
|
||||
version: Scalars['String'];
|
||||
workspaceId: Scalars['ID'];
|
||||
serverURL: Scalars['String'];
|
||||
anonymousAccessEnabled?: Maybe<Scalars['Boolean']>;
|
||||
/** @deprecated Field no longer supported */
|
||||
authenticationEnabled?: Maybe<Scalars['Boolean']>;
|
||||
@@ -952,6 +953,7 @@ export interface AdminPermissionInfo {
|
||||
|
||||
export interface ServerConfigInput {
|
||||
serverName?: Maybe<Scalars['String']>;
|
||||
serverURL?: Maybe<Scalars['String']>;
|
||||
adminName?: Maybe<Scalars['String']>;
|
||||
adminPassword?: Maybe<Scalars['String']>;
|
||||
anonymousAccessEnabled?: Maybe<Scalars['Boolean']>;
|
||||
@@ -1678,7 +1680,7 @@ export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export interface ServerConfigQuery {
|
||||
serverConfig: (
|
||||
Pick<ServerConfig, 'name' | 'version' | 'workspaceId' | 'productConfiguration' | 'supportsCustomConnections' | 'supportsConnectionBrowser' | 'supportsWorkspaces' | 'sessionExpireTime' | 'anonymousAccessEnabled' | 'adminCredentialsSaveEnabled' | 'publicCredentialsSaveEnabled' | 'licenseRequired' | 'licenseValid' | 'configurationMode' | 'developmentMode' | 'enabledAuthProviders'>
|
||||
Pick<ServerConfig, 'name' | 'version' | 'workspaceId' | 'serverURL' | 'productConfiguration' | 'supportsCustomConnections' | 'supportsConnectionBrowser' | 'supportsWorkspaces' | 'sessionExpireTime' | 'anonymousAccessEnabled' | 'adminCredentialsSaveEnabled' | 'publicCredentialsSaveEnabled' | 'licenseRequired' | 'licenseValid' | 'configurationMode' | 'developmentMode' | 'enabledAuthProviders'>
|
||||
& { supportedLanguages: Array<Pick<ServerLanguage, 'isoCode' | 'displayName' | 'nativeName'>>; defaultNavigatorSettings: AllNavigatorSettingsFragment; productInfo: Pick<ProductInfo, 'id' | 'version' | 'name' | 'description' | 'buildTime' | 'releaseTime' | 'licenseInfo'> }
|
||||
);
|
||||
}
|
||||
@@ -2691,6 +2693,7 @@ export const ServerConfigDocument = `
|
||||
name
|
||||
version
|
||||
workspaceId
|
||||
serverURL
|
||||
productConfiguration
|
||||
supportsCustomConnections
|
||||
supportsConnectionBrowser
|
||||
|
||||
+11
@@ -37,6 +37,17 @@ export const ServerConfigurationInfoForm: React.FC<Props> = observer(function Se
|
||||
>
|
||||
{translate('administration_configuration_wizard_configuration_server_name')}
|
||||
</InputFieldNew>
|
||||
<InputFieldNew
|
||||
title={translate('administration_configuration_wizard_configuration_server_url_description')}
|
||||
type="url"
|
||||
name="serverURL"
|
||||
state={state.serverConfig}
|
||||
mod='surface'
|
||||
required
|
||||
tiny
|
||||
>
|
||||
{translate('administration_configuration_wizard_configuration_server_url')}
|
||||
</InputFieldNew>
|
||||
<InputFieldNew
|
||||
title={translate('administration_configuration_wizard_configuration_server_session_lifetime_description')}
|
||||
type="number"
|
||||
|
||||
+6
@@ -161,6 +161,12 @@ export class ServerConfigurationService {
|
||||
}
|
||||
|
||||
data.state.serverConfig.serverName = config.name || config.productInfo.name;
|
||||
data.state.serverConfig.serverURL = config.serverURL;
|
||||
|
||||
if (this.administrationScreenService.isConfigurationMode) {
|
||||
data.state.serverConfig.serverURL = window.location.origin;
|
||||
}
|
||||
|
||||
data.state.serverConfig.sessionExpireTime = config.sessionExpireTime;
|
||||
|
||||
data.state.serverConfig.adminCredentialsSaveEnabled = config.adminCredentialsSaveEnabled;
|
||||
|
||||
@@ -18,6 +18,8 @@ export default [
|
||||
|
||||
['administration_configuration_wizard_configuration_server_info', 'Server Information'],
|
||||
['administration_configuration_wizard_configuration_server_name', 'Server Name'],
|
||||
['administration_configuration_wizard_configuration_server_url', 'Server URL'],
|
||||
['administration_configuration_wizard_configuration_server_url_description', 'Global access server url'],
|
||||
['administration_configuration_wizard_configuration_server_info_unsaved_title', 'Unsaved settings'],
|
||||
['administration_configuration_wizard_configuration_server_info_unsaved_message', 'Settings can be saved on Server configuration page'],
|
||||
['administration_configuration_wizard_configuration_server_info_unsaved_navigate', 'Open'],
|
||||
|
||||
@@ -53,7 +53,7 @@ export const UserInfo = observer(function UserInfo() {
|
||||
}
|
||||
|
||||
return styled(style)(
|
||||
<MenuTrigger panel={panel} style={[topMenuStyles]} disabled={hidden.get()}>
|
||||
<MenuTrigger panel={panel} style={[topMenuStyles]} disabled={hidden.get()} modal>
|
||||
<user>
|
||||
<user-icon>
|
||||
<IconOrImage icon='user' viewBox='0 0 28 28' />
|
||||
|
||||
@@ -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<Props> = observer(function DataGridLoader({
|
||||
export const DataGridLoader: React.FC<Props> = function DataGridLoader({
|
||||
tableModel, resultIndex, className,
|
||||
}) {
|
||||
return (
|
||||
@@ -35,4 +33,4 @@ export const DataGridLoader: React.FC<Props> = observer(function DataGridLoader(
|
||||
)}
|
||||
</ComplexLoader>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ export const CellMenu: React.FC<Props> = observer(function TreeNodeMenu({
|
||||
<MenuTrigger
|
||||
panel={panel}
|
||||
style={[cellMenuStyles]}
|
||||
modal
|
||||
onClick={() => dataGridContextMenuService.openMenu(model, resultIndex, row, column)}
|
||||
onVisibleSwitch={onStateSwitch}
|
||||
>
|
||||
|
||||
+1
@@ -86,6 +86,7 @@ export const TableFooterMenuItem = observer(function TableFooterMenuItem({
|
||||
panel={menuItem.panel}
|
||||
disabled={menuItem.isDisabled}
|
||||
style={[tableFooterMenuStyles]}
|
||||
modal
|
||||
>
|
||||
{menuItem.icon && (
|
||||
<menu-trigger-icon>
|
||||
|
||||
+1
-2
@@ -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)(
|
||||
<tab-wrapper as="div">
|
||||
<tab-wrapper>
|
||||
<ObjectChildrenPropertyTable nodeIds={folderMixin.getChildrenId()} />
|
||||
</tab-wrapper>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user